@hugobatist/smartcode 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/cli.js +4324 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.js +1167 -0
- package/dist/index.js.map +1 -0
- package/dist/static/annotations-panel.js +133 -0
- package/dist/static/annotations-svg.js +108 -0
- package/dist/static/annotations.css +367 -0
- package/dist/static/annotations.js +367 -0
- package/dist/static/app-init.js +497 -0
- package/dist/static/breakpoints.css +69 -0
- package/dist/static/breakpoints.js +197 -0
- package/dist/static/clipboard.js +94 -0
- package/dist/static/collapse-ui.js +325 -0
- package/dist/static/command-history.js +89 -0
- package/dist/static/context-menu.js +334 -0
- package/dist/static/custom-renderer.js +201 -0
- package/dist/static/dagre-layout.js +291 -0
- package/dist/static/diagram-dom.js +241 -0
- package/dist/static/diagram-editor.js +368 -0
- package/dist/static/editor-panel.js +107 -0
- package/dist/static/editor-popovers.js +187 -0
- package/dist/static/event-bus.js +57 -0
- package/dist/static/export.js +181 -0
- package/dist/static/file-tree.js +470 -0
- package/dist/static/ghost-paths.js +397 -0
- package/dist/static/heatmap.css +116 -0
- package/dist/static/heatmap.js +308 -0
- package/dist/static/icons.js +66 -0
- package/dist/static/inline-edit.js +294 -0
- package/dist/static/interaction-state.js +155 -0
- package/dist/static/interaction-tracker.js +93 -0
- package/dist/static/live.html +239 -0
- package/dist/static/main-layout.css +220 -0
- package/dist/static/main.css +334 -0
- package/dist/static/mcp-sessions.js +202 -0
- package/dist/static/modal.css +109 -0
- package/dist/static/modal.js +171 -0
- package/dist/static/node-drag.js +293 -0
- package/dist/static/pan-zoom.js +199 -0
- package/dist/static/renderer.js +280 -0
- package/dist/static/search.css +103 -0
- package/dist/static/search.js +304 -0
- package/dist/static/selection.js +353 -0
- package/dist/static/session-player.css +137 -0
- package/dist/static/session-player.js +411 -0
- package/dist/static/sidebar.css +248 -0
- package/dist/static/svg-renderer.js +313 -0
- package/dist/static/svg-shapes.js +218 -0
- package/dist/static/tokens.css +76 -0
- package/dist/static/vendor/dagre-bundle.js +43 -0
- package/dist/static/vendor/dagre.min.js +3 -0
- package/dist/static/vendor/graphlib.min.js +2 -0
- package/dist/static/viewport-transform.js +107 -0
- package/dist/static/workspace-switcher.js +202 -0
- package/dist/static/ws-client.js +71 -0
- package/dist/static/ws-handler.js +125 -0
- package/package.json +74 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCode Dagre Layout -- graph layout computation using dagre.
|
|
3
|
+
* Converts a GraphModel JSON (from /api/graph/:file) into positioned nodes,
|
|
4
|
+
* edges with routed points, and subgraph bounding boxes.
|
|
5
|
+
*
|
|
6
|
+
* Dependencies: dagre (loaded via CDN in live.html)
|
|
7
|
+
* Dependents: canvas-renderer.js (Plan 02)
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* var result = SmartCodeDagreLayout.computeLayout(graphModel);
|
|
11
|
+
* // result => { width, height, nodes[], edges[], subgraphs[] }
|
|
12
|
+
*/
|
|
13
|
+
(function() {
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
// ── Text measurement (lazy canvas init) ──
|
|
17
|
+
|
|
18
|
+
var _measureCanvas = null;
|
|
19
|
+
|
|
20
|
+
function getMeasureContext() {
|
|
21
|
+
if (!_measureCanvas) {
|
|
22
|
+
_measureCanvas = document.createElement('canvas');
|
|
23
|
+
}
|
|
24
|
+
return _measureCanvas.getContext('2d');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Measure the pixel width of a text string using an offscreen canvas.
|
|
29
|
+
* @param {string} text - The text to measure.
|
|
30
|
+
* @param {string} [font] - CSS font string. Defaults to '600 15px Inter, sans-serif'.
|
|
31
|
+
* @returns {number} Width in pixels.
|
|
32
|
+
*/
|
|
33
|
+
function measureTextWidth(text, font) {
|
|
34
|
+
var ctx = getMeasureContext();
|
|
35
|
+
ctx.font = font || '600 15px Inter, sans-serif';
|
|
36
|
+
return ctx.measureText(text).width;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calculate node dimensions based on label text and shape type.
|
|
41
|
+
* Returns { width, height } with shape-specific adjustments.
|
|
42
|
+
* @param {string} label - Node label text.
|
|
43
|
+
* @param {string} shape - Node shape (rect, circle, diamond, etc.).
|
|
44
|
+
* @returns {{ width: number, height: number }}
|
|
45
|
+
*/
|
|
46
|
+
function measureNodeDimensions(label, shape) {
|
|
47
|
+
// Handle multiline labels: measure widest line, stack lines vertically
|
|
48
|
+
var labelStr = (label || '').replace(/\\n/g, '\n');
|
|
49
|
+
var lines = labelStr.split('\n');
|
|
50
|
+
var maxLineW = 0;
|
|
51
|
+
for (var li = 0; li < lines.length; li++) {
|
|
52
|
+
var lw = measureTextWidth(lines[li]);
|
|
53
|
+
if (lw > maxLineW) maxLineW = lw;
|
|
54
|
+
}
|
|
55
|
+
var textW = maxLineW;
|
|
56
|
+
var hPad = 32;
|
|
57
|
+
var lineH = 24;
|
|
58
|
+
var vPad = 24;
|
|
59
|
+
var w = textW + hPad;
|
|
60
|
+
var h = lineH * lines.length + vPad;
|
|
61
|
+
|
|
62
|
+
switch (shape) {
|
|
63
|
+
case 'circle':
|
|
64
|
+
var diameter = Math.max(w, h) + 8;
|
|
65
|
+
w = diameter;
|
|
66
|
+
h = diameter;
|
|
67
|
+
break;
|
|
68
|
+
case 'diamond':
|
|
69
|
+
w = w * 1.4;
|
|
70
|
+
h = h * 1.4;
|
|
71
|
+
break;
|
|
72
|
+
case 'hexagon':
|
|
73
|
+
w = w + h / 2;
|
|
74
|
+
break;
|
|
75
|
+
case 'cylinder':
|
|
76
|
+
h = h + 16;
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { width: Math.ceil(w), height: Math.ceil(h) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Subgraph membership lookup ──
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build a Set of all subgraph IDs for quick lookup.
|
|
89
|
+
* @param {Object} graphModel - The graph model from /api/graph/:file.
|
|
90
|
+
* @returns {Set<string>}
|
|
91
|
+
*/
|
|
92
|
+
function buildSubgraphIdSet(graphModel) {
|
|
93
|
+
var ids = {};
|
|
94
|
+
var entries = Object.keys(graphModel.subgraphs || {});
|
|
95
|
+
for (var i = 0; i < entries.length; i++) {
|
|
96
|
+
ids[entries[i]] = true;
|
|
97
|
+
}
|
|
98
|
+
return ids;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a subgraph endpoint to its first child node (dagre bug #238 workaround).
|
|
103
|
+
* When an edge references a subgraph ID, dagre cannot route it properly in
|
|
104
|
+
* compound mode. We redirect the edge to the first node inside the subgraph.
|
|
105
|
+
* @param {string} endpoint - Node or subgraph ID.
|
|
106
|
+
* @param {Object} subgraphIds - Lookup of subgraph IDs.
|
|
107
|
+
* @param {Object} subgraphs - The subgraphs map from graphModel.
|
|
108
|
+
* @returns {string} The resolved node ID.
|
|
109
|
+
*/
|
|
110
|
+
function resolveEndpoint(endpoint, subgraphIds, subgraphs) {
|
|
111
|
+
if (subgraphIds[endpoint] && subgraphs[endpoint]) {
|
|
112
|
+
var sg = subgraphs[endpoint];
|
|
113
|
+
if (sg.nodeIds && sg.nodeIds.length > 0) {
|
|
114
|
+
return sg.nodeIds[0];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return endpoint;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Layout computation ──
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Compute a full layout for a GraphModel using dagre.
|
|
124
|
+
* @param {Object} graphModel - The graph model JSON from /api/graph/:file.
|
|
125
|
+
* @returns {{ width: number, height: number, nodes: Array, edges: Array, subgraphs: Array }}
|
|
126
|
+
*/
|
|
127
|
+
function computeLayout(graphModel) {
|
|
128
|
+
/* global dagre */
|
|
129
|
+
var g = new dagre.graphlib.Graph({ compound: true });
|
|
130
|
+
|
|
131
|
+
g.setGraph({
|
|
132
|
+
rankdir: graphModel.direction || 'TB',
|
|
133
|
+
nodesep: 80,
|
|
134
|
+
ranksep: 100,
|
|
135
|
+
edgesep: 20,
|
|
136
|
+
marginx: 20,
|
|
137
|
+
marginy: 20,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
g.setDefaultEdgeLabel(function() { return {}; });
|
|
141
|
+
|
|
142
|
+
// -- Add nodes --
|
|
143
|
+
var nodeEntries = Object.entries(graphModel.nodes || {});
|
|
144
|
+
for (var ni = 0; ni < nodeEntries.length; ni++) {
|
|
145
|
+
var nodeId = nodeEntries[ni][0];
|
|
146
|
+
var node = nodeEntries[ni][1];
|
|
147
|
+
var dims = measureNodeDimensions(node.label || nodeId, node.shape || 'rect');
|
|
148
|
+
g.setNode(nodeId, {
|
|
149
|
+
label: node.label || nodeId,
|
|
150
|
+
width: dims.width,
|
|
151
|
+
height: dims.height,
|
|
152
|
+
shape: node.shape || 'rect',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// -- Add subgraphs --
|
|
157
|
+
var sgEntries = Object.entries(graphModel.subgraphs || {});
|
|
158
|
+
var subgraphIds = buildSubgraphIdSet(graphModel);
|
|
159
|
+
|
|
160
|
+
for (var si = 0; si < sgEntries.length; si++) {
|
|
161
|
+
var sgId = sgEntries[si][0];
|
|
162
|
+
var sg = sgEntries[si][1];
|
|
163
|
+
g.setNode(sgId, {
|
|
164
|
+
label: sg.label || sgId,
|
|
165
|
+
clusterLabelPos: 'top',
|
|
166
|
+
style: 'subgraph',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Parent child nodes inside this subgraph
|
|
170
|
+
var sgNodeIds = sg.nodeIds || [];
|
|
171
|
+
for (var sni = 0; sni < sgNodeIds.length; sni++) {
|
|
172
|
+
g.setParent(sgNodeIds[sni], sgId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Nested subgraphs
|
|
176
|
+
if (sg.parentId) {
|
|
177
|
+
g.setParent(sgId, sg.parentId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// -- Add edges --
|
|
182
|
+
var edges = graphModel.edges || [];
|
|
183
|
+
var subgraphs = graphModel.subgraphs || {};
|
|
184
|
+
|
|
185
|
+
for (var ei = 0; ei < edges.length; ei++) {
|
|
186
|
+
var edge = edges[ei];
|
|
187
|
+
var from = resolveEndpoint(edge.from, subgraphIds, subgraphs);
|
|
188
|
+
var to = resolveEndpoint(edge.to, subgraphIds, subgraphs);
|
|
189
|
+
|
|
190
|
+
var edgeLabel = edge.label || '';
|
|
191
|
+
var edgeLabelW = edgeLabel ? measureTextWidth(edgeLabel) + 16 : 0;
|
|
192
|
+
var edgeLabelH = edgeLabel ? 20 : 0;
|
|
193
|
+
|
|
194
|
+
g.setEdge(from, to, {
|
|
195
|
+
label: edgeLabel,
|
|
196
|
+
width: edgeLabelW,
|
|
197
|
+
height: edgeLabelH,
|
|
198
|
+
labelpos: 'c',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// -- Run dagre layout --
|
|
203
|
+
dagre.layout(g);
|
|
204
|
+
|
|
205
|
+
// -- Extract results --
|
|
206
|
+
return extractLayoutResult(g, graphModel);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Extract positioned layout data from a dagre graph after layout().
|
|
211
|
+
* @param {Object} g - The dagre graph after layout.
|
|
212
|
+
* @param {Object} graphModel - The original graph model (for metadata).
|
|
213
|
+
* @returns {{ width: number, height: number, nodes: Array, edges: Array, subgraphs: Array }}
|
|
214
|
+
*/
|
|
215
|
+
function extractLayoutResult(g, graphModel) {
|
|
216
|
+
var graphInfo = g.graph();
|
|
217
|
+
var subgraphIds = buildSubgraphIdSet(graphModel);
|
|
218
|
+
|
|
219
|
+
var layoutNodes = [];
|
|
220
|
+
var layoutSubgraphs = [];
|
|
221
|
+
|
|
222
|
+
var allNodeIds = g.nodes();
|
|
223
|
+
for (var i = 0; i < allNodeIds.length; i++) {
|
|
224
|
+
var nid = allNodeIds[i];
|
|
225
|
+
var ndata = g.node(nid);
|
|
226
|
+
if (!ndata) continue;
|
|
227
|
+
|
|
228
|
+
if (subgraphIds[nid]) {
|
|
229
|
+
layoutSubgraphs.push({
|
|
230
|
+
id: nid,
|
|
231
|
+
label: ndata.label || nid,
|
|
232
|
+
x: ndata.x,
|
|
233
|
+
y: ndata.y,
|
|
234
|
+
width: ndata.width,
|
|
235
|
+
height: ndata.height,
|
|
236
|
+
});
|
|
237
|
+
} else {
|
|
238
|
+
layoutNodes.push({
|
|
239
|
+
id: nid,
|
|
240
|
+
label: ndata.label || nid,
|
|
241
|
+
shape: ndata.shape || 'rect',
|
|
242
|
+
x: ndata.x,
|
|
243
|
+
y: ndata.y,
|
|
244
|
+
width: ndata.width,
|
|
245
|
+
height: ndata.height,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var layoutEdges = [];
|
|
251
|
+
var dagreEdges = g.edges();
|
|
252
|
+
for (var j = 0; j < dagreEdges.length; j++) {
|
|
253
|
+
var de = dagreEdges[j];
|
|
254
|
+
var edata = g.edge(de);
|
|
255
|
+
if (!edata) continue;
|
|
256
|
+
|
|
257
|
+
// Find the original edge data for type/id
|
|
258
|
+
var origEdges = graphModel.edges || [];
|
|
259
|
+
var origEdge = null;
|
|
260
|
+
for (var k = 0; k < origEdges.length; k++) {
|
|
261
|
+
if (origEdges[k].from === de.v && origEdges[k].to === de.w) {
|
|
262
|
+
origEdge = origEdges[k];
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
layoutEdges.push({
|
|
268
|
+
id: origEdge ? origEdge.id : (de.v + '->' + de.w),
|
|
269
|
+
from: de.v,
|
|
270
|
+
to: de.w,
|
|
271
|
+
label: edata.label || '',
|
|
272
|
+
type: origEdge ? origEdge.type : 'arrow',
|
|
273
|
+
points: edata.points || [],
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
width: graphInfo.width || 0,
|
|
279
|
+
height: graphInfo.height || 0,
|
|
280
|
+
nodes: layoutNodes,
|
|
281
|
+
edges: layoutEdges,
|
|
282
|
+
subgraphs: layoutSubgraphs,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Public API ──
|
|
287
|
+
window.SmartCodeDagreLayout = {
|
|
288
|
+
computeLayout: computeLayout,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
})();
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DiagramDOM — abstraction layer for SVG DOM queries.
|
|
3
|
+
* Supports both Mermaid-rendered SVGs and custom SmartCode SVGs.
|
|
4
|
+
* Consolidates SVG element lookups duplicated across annotations.js,
|
|
5
|
+
* collapse-ui.js, search.js, and diagram-editor.js.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: Never cache SVG element references — render()
|
|
8
|
+
* replaces the entire SVG via innerHTML, invalidating all references.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* DiagramDOM.getSVG();
|
|
12
|
+
* DiagramDOM.getRendererType(); // 'custom' | 'mermaid'
|
|
13
|
+
* DiagramDOM.findNodeElement('myNode');
|
|
14
|
+
* DiagramDOM.extractNodeId(clickedElement);
|
|
15
|
+
*/
|
|
16
|
+
(function() {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
var NODE_RE = /^flowchart-(.+)-\d+$/;
|
|
20
|
+
var SUBGRAPH_RE = /^subGraph\d+-(.+)-\d+$/;
|
|
21
|
+
var EDGE_RE = /^L-(.+)$/;
|
|
22
|
+
|
|
23
|
+
var DiagramDOM = {
|
|
24
|
+
/**
|
|
25
|
+
* Returns the current SVG element, or null.
|
|
26
|
+
*/
|
|
27
|
+
getSVG: function() {
|
|
28
|
+
return document.querySelector('#preview svg');
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detects whether the current SVG is from the custom renderer.
|
|
33
|
+
* Checks for the `.smartcode-diagram` class on the root <g> element.
|
|
34
|
+
* @returns {'custom'|'mermaid'}
|
|
35
|
+
*/
|
|
36
|
+
getRendererType: function() {
|
|
37
|
+
var svg = this.getSVG();
|
|
38
|
+
if (!svg) return 'mermaid';
|
|
39
|
+
return svg.querySelector('.smartcode-diagram') ? 'custom' : 'mermaid';
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Finds the SVG element for a given node ID.
|
|
44
|
+
* Tries data-node-id attribute first (custom), then Mermaid regex.
|
|
45
|
+
*/
|
|
46
|
+
findNodeElement: function(nodeId) {
|
|
47
|
+
var svg = this.getSVG();
|
|
48
|
+
if (!svg) return null;
|
|
49
|
+
// Custom renderer: data-node-id attribute
|
|
50
|
+
var custom = svg.querySelector('[data-node-id="' + nodeId + '"]');
|
|
51
|
+
if (custom) return custom;
|
|
52
|
+
// Mermaid: regex on element id attributes
|
|
53
|
+
var elements = svg.querySelectorAll('[id]');
|
|
54
|
+
for (var i = 0; i < elements.length; i++) {
|
|
55
|
+
var el = elements[i];
|
|
56
|
+
var id = el.getAttribute('id');
|
|
57
|
+
var match = id ? id.match(NODE_RE) : null;
|
|
58
|
+
if (match && match[1] === nodeId) return el;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Finds the SVG element for a given subgraph ID.
|
|
65
|
+
* Matches /^subGraph\d+-(.+)-\d+$/.
|
|
66
|
+
*/
|
|
67
|
+
findSubgraphElement: function(subgraphId) {
|
|
68
|
+
var svg = this.getSVG();
|
|
69
|
+
if (!svg) return null;
|
|
70
|
+
// Custom renderer: data-subgraph-id attribute
|
|
71
|
+
var custom = svg.querySelector('[data-subgraph-id="' + subgraphId + '"]');
|
|
72
|
+
if (custom) return custom;
|
|
73
|
+
// Mermaid: regex on element id attributes
|
|
74
|
+
var elements = svg.querySelectorAll('[id]');
|
|
75
|
+
for (var i = 0; i < elements.length; i++) {
|
|
76
|
+
var el = elements[i];
|
|
77
|
+
var id = el.getAttribute('id');
|
|
78
|
+
var match = id ? id.match(SUBGRAPH_RE) : null;
|
|
79
|
+
if (match && match[1] === subgraphId) return el;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Walks up the DOM from an element to find node/edge/subgraph identity.
|
|
86
|
+
* Consolidates duplicated logic from annotations.js, collapse-ui.js,
|
|
87
|
+
* search.js, and diagram-editor.js.
|
|
88
|
+
*
|
|
89
|
+
* Returns: { type: 'node'|'edge'|'subgraph', id: string } or null.
|
|
90
|
+
*/
|
|
91
|
+
extractNodeId: function(element) {
|
|
92
|
+
var el = element;
|
|
93
|
+
while (el && el !== document.body) {
|
|
94
|
+
// Custom renderer: check data attributes first
|
|
95
|
+
if (el.getAttribute) {
|
|
96
|
+
var dataNodeId = el.getAttribute('data-node-id');
|
|
97
|
+
if (dataNodeId) return { type: 'node', id: dataNodeId };
|
|
98
|
+
var dataEdgeId = el.getAttribute('data-edge-id');
|
|
99
|
+
if (dataEdgeId) return { type: 'edge', id: dataEdgeId };
|
|
100
|
+
var dataSubgraphId = el.getAttribute('data-subgraph-id');
|
|
101
|
+
if (dataSubgraphId) return { type: 'subgraph', id: dataSubgraphId };
|
|
102
|
+
}
|
|
103
|
+
// Mermaid: regex patterns on id attribute
|
|
104
|
+
var id = el.getAttribute ? el.getAttribute('id') : null;
|
|
105
|
+
if (id) {
|
|
106
|
+
var nodeMatch = id.match(NODE_RE);
|
|
107
|
+
if (nodeMatch) return { type: 'node', id: nodeMatch[1] };
|
|
108
|
+
var edgeMatch = id.match(EDGE_RE);
|
|
109
|
+
if (edgeMatch) return { type: 'edge', id: 'L-' + edgeMatch[1] };
|
|
110
|
+
var subMatch = id.match(SUBGRAPH_RE);
|
|
111
|
+
if (subMatch) return { type: 'subgraph', id: subMatch[1] };
|
|
112
|
+
}
|
|
113
|
+
el = el.parentElement;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns getBBox() of the found node element, or null.
|
|
120
|
+
*/
|
|
121
|
+
getNodeBBox: function(nodeId) {
|
|
122
|
+
var el = this.findNodeElement(nodeId);
|
|
123
|
+
if (!el || !el.getBBox) return null;
|
|
124
|
+
return el.getBBox();
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Returns the .nodeLabel textContent within a node, or null.
|
|
129
|
+
*/
|
|
130
|
+
getNodeLabel: function(nodeId) {
|
|
131
|
+
var el = this.findNodeElement(nodeId);
|
|
132
|
+
if (!el) return null;
|
|
133
|
+
// Custom renderer: direct child <text> element
|
|
134
|
+
if (el.getAttribute('data-node-id')) {
|
|
135
|
+
var textEl = el.querySelector('text');
|
|
136
|
+
return textEl ? textEl.textContent : null;
|
|
137
|
+
}
|
|
138
|
+
// Mermaid: .nodeLabel span
|
|
139
|
+
var label = el.querySelector('.nodeLabel');
|
|
140
|
+
return label ? label.textContent : null;
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Returns all node label elements from the SVG.
|
|
145
|
+
* Custom renderer: <text> children of .smartcode-node
|
|
146
|
+
* Mermaid: .nodeLabel elements
|
|
147
|
+
*/
|
|
148
|
+
getAllNodeLabels: function() {
|
|
149
|
+
var svg = this.getSVG();
|
|
150
|
+
if (!svg) return [];
|
|
151
|
+
if (this.getRendererType() === 'custom') {
|
|
152
|
+
return Array.from(svg.querySelectorAll('.smartcode-node > text'));
|
|
153
|
+
}
|
|
154
|
+
return Array.from(svg.querySelectorAll('.nodeLabel'));
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Walks up to find .node, .cluster, or .smartcode-edge parent element.
|
|
159
|
+
*/
|
|
160
|
+
findMatchParent: function(element) {
|
|
161
|
+
var current = element;
|
|
162
|
+
while (current && current.tagName !== 'svg') {
|
|
163
|
+
if (current.classList &&
|
|
164
|
+
(current.classList.contains('node') ||
|
|
165
|
+
current.classList.contains('cluster') ||
|
|
166
|
+
current.classList.contains('smartcode-node') ||
|
|
167
|
+
current.classList.contains('smartcode-subgraph') ||
|
|
168
|
+
current.classList.contains('smartcode-edge'))) {
|
|
169
|
+
return current;
|
|
170
|
+
}
|
|
171
|
+
current = current.parentElement;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Finds the SVG element for a given edge ID.
|
|
178
|
+
* Custom: data-edge-id attribute
|
|
179
|
+
* Mermaid: id="L-{edgeId}"
|
|
180
|
+
*/
|
|
181
|
+
findEdgeElement: function(edgeId) {
|
|
182
|
+
var svg = this.getSVG();
|
|
183
|
+
if (!svg) return null;
|
|
184
|
+
// Custom renderer: data-edge-id attribute
|
|
185
|
+
var custom = svg.querySelector('[data-edge-id="' + edgeId + '"]');
|
|
186
|
+
if (custom) return custom;
|
|
187
|
+
// Mermaid: id="L-{edgeId}"
|
|
188
|
+
var mermaid = svg.querySelector('[id="L-' + edgeId + '"]');
|
|
189
|
+
return mermaid || null;
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Adds/removes outline styling on a node.
|
|
194
|
+
*/
|
|
195
|
+
highlightNode: function(nodeId, on) {
|
|
196
|
+
var el = this.findNodeElement(nodeId);
|
|
197
|
+
if (!el) return;
|
|
198
|
+
// SVG elements don't support CSS outline — modify shape stroke instead
|
|
199
|
+
var shape = el.querySelector('rect, circle, polygon, path, ellipse');
|
|
200
|
+
if (!shape) {
|
|
201
|
+
var childG = el.querySelector('g');
|
|
202
|
+
if (childG) shape = childG.querySelector('rect, circle, polygon, path, ellipse');
|
|
203
|
+
}
|
|
204
|
+
if (shape) {
|
|
205
|
+
if (on) {
|
|
206
|
+
shape.setAttribute('data-prev-stroke', shape.getAttribute('stroke') || '');
|
|
207
|
+
shape.setAttribute('data-prev-stroke-width', shape.getAttribute('stroke-width') || '');
|
|
208
|
+
shape.setAttribute('stroke', '#3b82f6');
|
|
209
|
+
shape.setAttribute('stroke-width', '3');
|
|
210
|
+
} else {
|
|
211
|
+
shape.setAttribute('stroke', shape.getAttribute('data-prev-stroke') || '');
|
|
212
|
+
shape.setAttribute('stroke-width', shape.getAttribute('data-prev-stroke-width') || '');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Returns all node elements from the SVG.
|
|
219
|
+
* Custom: .smartcode-node elements; Mermaid: .node elements
|
|
220
|
+
*/
|
|
221
|
+
getAllNodeElements: function() {
|
|
222
|
+
var svg = this.getSVG();
|
|
223
|
+
if (!svg) return [];
|
|
224
|
+
if (this.getRendererType() === 'custom') {
|
|
225
|
+
return Array.from(svg.querySelectorAll('.smartcode-node'));
|
|
226
|
+
}
|
|
227
|
+
return Array.from(svg.querySelectorAll('.node'));
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Returns SVG viewBox baseVal, or null.
|
|
232
|
+
*/
|
|
233
|
+
getViewBox: function() {
|
|
234
|
+
var svg = this.getSVG();
|
|
235
|
+
if (!svg) return null;
|
|
236
|
+
return (svg.viewBox && svg.viewBox.baseVal) ? svg.viewBox.baseVal : null;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
window.DiagramDOM = DiagramDOM;
|
|
241
|
+
})();
|