@diagrammo/dgmo 0.2.6 → 0.2.7
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/README.md +213 -57
- package/dist/cli.cjs +92 -86
- package/dist/index.cjs +1337 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -2
- package/dist/index.d.ts +107 -2
- package/dist/index.js +1332 -193
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/d3.ts +59 -1
- package/src/dgmo-router.ts +5 -1
- package/src/echarts.ts +2 -2
- package/src/graph/flowchart-parser.ts +499 -0
- package/src/graph/flowchart-renderer.ts +503 -0
- package/src/graph/layout.ts +222 -0
- package/src/graph/types.ts +44 -0
- package/src/index.ts +24 -0
- package/src/sequence/parser.ts +229 -37
- package/src/sequence/renderer.ts +310 -16
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Flowchart SVG Renderer
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import * as d3Selection from 'd3-selection';
|
|
6
|
+
import * as d3Shape from 'd3-shape';
|
|
7
|
+
import { FONT_FAMILY } from '../fonts';
|
|
8
|
+
import type { PaletteColors } from '../palettes';
|
|
9
|
+
import type { ParsedGraph } from './types';
|
|
10
|
+
import type { LayoutResult, LayoutNode, LayoutEdge, LayoutGroup } from './layout';
|
|
11
|
+
import { parseFlowchart } from './flowchart-parser';
|
|
12
|
+
import { layoutGraph } from './layout';
|
|
13
|
+
|
|
14
|
+
// ============================================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
const DIAGRAM_PADDING = 20;
|
|
19
|
+
const TITLE_HEIGHT = 30;
|
|
20
|
+
const TITLE_FONT_SIZE = 18;
|
|
21
|
+
const NODE_FONT_SIZE = 13;
|
|
22
|
+
const EDGE_LABEL_FONT_SIZE = 11;
|
|
23
|
+
const GROUP_LABEL_FONT_SIZE = 11;
|
|
24
|
+
const EDGE_STROKE_WIDTH = 1.5;
|
|
25
|
+
const NODE_STROKE_WIDTH = 1.5;
|
|
26
|
+
const ARROWHEAD_W = 10;
|
|
27
|
+
const ARROWHEAD_H = 7;
|
|
28
|
+
const IO_SKEW = 15;
|
|
29
|
+
const SUBROUTINE_INSET = 8;
|
|
30
|
+
const DOC_WAVE_HEIGHT = 10;
|
|
31
|
+
const GROUP_EXTRA_PADDING = 12;
|
|
32
|
+
|
|
33
|
+
// ============================================================
|
|
34
|
+
// Color helpers (inline mix to avoid cross-module import issues)
|
|
35
|
+
// ============================================================
|
|
36
|
+
|
|
37
|
+
function mix(a: string, b: string, pct: number): string {
|
|
38
|
+
const parse = (h: string) => {
|
|
39
|
+
const r = h.replace('#', '');
|
|
40
|
+
const f = r.length === 3 ? r[0]+r[0]+r[1]+r[1]+r[2]+r[2] : r;
|
|
41
|
+
return [parseInt(f.substring(0,2),16), parseInt(f.substring(2,4),16), parseInt(f.substring(4,6),16)];
|
|
42
|
+
};
|
|
43
|
+
const [ar,ag,ab] = parse(a), [br,bg,bb] = parse(b), t = pct/100;
|
|
44
|
+
const c = (x: number, y: number) => Math.round(x*t + y*(1-t)).toString(16).padStart(2,'0');
|
|
45
|
+
return `#${c(ar,br)}${c(ag,bg)}${c(ab,bb)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nodeFill(palette: PaletteColors, isDark: boolean, nodeColor?: string): string {
|
|
49
|
+
if (nodeColor) {
|
|
50
|
+
return mix(nodeColor, isDark ? palette.surface : palette.bg, 25);
|
|
51
|
+
}
|
|
52
|
+
return mix(palette.primary, isDark ? palette.surface : palette.bg, 15);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function nodeStroke(palette: PaletteColors, nodeColor?: string): string {
|
|
56
|
+
return nodeColor ?? palette.textMuted;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================
|
|
60
|
+
// Shape renderers
|
|
61
|
+
// ============================================================
|
|
62
|
+
|
|
63
|
+
type GSelection = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
64
|
+
|
|
65
|
+
function renderTerminal(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
66
|
+
const w = node.width;
|
|
67
|
+
const h = node.height;
|
|
68
|
+
const rx = h / 2;
|
|
69
|
+
g.append('rect')
|
|
70
|
+
.attr('x', -w / 2)
|
|
71
|
+
.attr('y', -h / 2)
|
|
72
|
+
.attr('width', w)
|
|
73
|
+
.attr('height', h)
|
|
74
|
+
.attr('rx', rx)
|
|
75
|
+
.attr('ry', rx)
|
|
76
|
+
.attr('fill', nodeFill(palette, isDark, node.color))
|
|
77
|
+
.attr('stroke', nodeStroke(palette, node.color))
|
|
78
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderProcess(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
82
|
+
const w = node.width;
|
|
83
|
+
const h = node.height;
|
|
84
|
+
g.append('rect')
|
|
85
|
+
.attr('x', -w / 2)
|
|
86
|
+
.attr('y', -h / 2)
|
|
87
|
+
.attr('width', w)
|
|
88
|
+
.attr('height', h)
|
|
89
|
+
.attr('rx', 3)
|
|
90
|
+
.attr('ry', 3)
|
|
91
|
+
.attr('fill', nodeFill(palette, isDark, node.color))
|
|
92
|
+
.attr('stroke', nodeStroke(palette, node.color))
|
|
93
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderDecision(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
97
|
+
const w = node.width / 2;
|
|
98
|
+
const h = node.height / 2;
|
|
99
|
+
const points = [
|
|
100
|
+
`${0},${-h}`, // top
|
|
101
|
+
`${w},${0}`, // right
|
|
102
|
+
`${0},${h}`, // bottom
|
|
103
|
+
`${-w},${0}`, // left
|
|
104
|
+
].join(' ');
|
|
105
|
+
g.append('polygon')
|
|
106
|
+
.attr('points', points)
|
|
107
|
+
.attr('fill', nodeFill(palette, isDark, node.color))
|
|
108
|
+
.attr('stroke', nodeStroke(palette, node.color))
|
|
109
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderIO(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
113
|
+
const w = node.width / 2;
|
|
114
|
+
const h = node.height / 2;
|
|
115
|
+
const sk = IO_SKEW;
|
|
116
|
+
const points = [
|
|
117
|
+
`${-w + sk},${-h}`, // top-left (shifted right)
|
|
118
|
+
`${w + sk},${-h}`, // top-right (shifted right)
|
|
119
|
+
`${w - sk},${h}`, // bottom-right (shifted left)
|
|
120
|
+
`${-w - sk},${h}`, // bottom-left (shifted left)
|
|
121
|
+
].join(' ');
|
|
122
|
+
g.append('polygon')
|
|
123
|
+
.attr('points', points)
|
|
124
|
+
.attr('fill', nodeFill(palette, isDark, node.color))
|
|
125
|
+
.attr('stroke', nodeStroke(palette, node.color))
|
|
126
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderSubroutine(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
130
|
+
const w = node.width;
|
|
131
|
+
const h = node.height;
|
|
132
|
+
const s = nodeStroke(palette, node.color);
|
|
133
|
+
// Outer rectangle
|
|
134
|
+
g.append('rect')
|
|
135
|
+
.attr('x', -w / 2)
|
|
136
|
+
.attr('y', -h / 2)
|
|
137
|
+
.attr('width', w)
|
|
138
|
+
.attr('height', h)
|
|
139
|
+
.attr('rx', 3)
|
|
140
|
+
.attr('ry', 3)
|
|
141
|
+
.attr('fill', nodeFill(palette, isDark, node.color))
|
|
142
|
+
.attr('stroke', s)
|
|
143
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
144
|
+
// Left inner border
|
|
145
|
+
g.append('line')
|
|
146
|
+
.attr('x1', -w / 2 + SUBROUTINE_INSET)
|
|
147
|
+
.attr('y1', -h / 2)
|
|
148
|
+
.attr('x2', -w / 2 + SUBROUTINE_INSET)
|
|
149
|
+
.attr('y2', h / 2)
|
|
150
|
+
.attr('stroke', s)
|
|
151
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
152
|
+
// Right inner border
|
|
153
|
+
g.append('line')
|
|
154
|
+
.attr('x1', w / 2 - SUBROUTINE_INSET)
|
|
155
|
+
.attr('y1', -h / 2)
|
|
156
|
+
.attr('x2', w / 2 - SUBROUTINE_INSET)
|
|
157
|
+
.attr('y2', h / 2)
|
|
158
|
+
.attr('stroke', s)
|
|
159
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderDocument(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
163
|
+
const w = node.width;
|
|
164
|
+
const h = node.height;
|
|
165
|
+
const waveH = DOC_WAVE_HEIGHT;
|
|
166
|
+
const left = -w / 2;
|
|
167
|
+
const right = w / 2;
|
|
168
|
+
const top = -h / 2;
|
|
169
|
+
const bottom = h / 2 - waveH;
|
|
170
|
+
|
|
171
|
+
// Path: straight top, straight right side, wavy bottom, straight left side
|
|
172
|
+
const d = [
|
|
173
|
+
`M ${left} ${top}`,
|
|
174
|
+
`L ${right} ${top}`,
|
|
175
|
+
`L ${right} ${bottom}`,
|
|
176
|
+
`C ${right - w * 0.25} ${bottom + waveH * 2}, ${left + w * 0.25} ${bottom - waveH}, ${left} ${bottom}`,
|
|
177
|
+
'Z',
|
|
178
|
+
].join(' ');
|
|
179
|
+
|
|
180
|
+
g.append('path')
|
|
181
|
+
.attr('d', d)
|
|
182
|
+
.attr('fill', nodeFill(palette, isDark, node.color))
|
|
183
|
+
.attr('stroke', nodeStroke(palette, node.color))
|
|
184
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderNodeShape(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
188
|
+
switch (node.shape) {
|
|
189
|
+
case 'terminal':
|
|
190
|
+
renderTerminal(g, node, palette, isDark);
|
|
191
|
+
break;
|
|
192
|
+
case 'process':
|
|
193
|
+
renderProcess(g, node, palette, isDark);
|
|
194
|
+
break;
|
|
195
|
+
case 'decision':
|
|
196
|
+
renderDecision(g, node, palette, isDark);
|
|
197
|
+
break;
|
|
198
|
+
case 'io':
|
|
199
|
+
renderIO(g, node, palette, isDark);
|
|
200
|
+
break;
|
|
201
|
+
case 'subroutine':
|
|
202
|
+
renderSubroutine(g, node, palette, isDark);
|
|
203
|
+
break;
|
|
204
|
+
case 'document':
|
|
205
|
+
renderDocument(g, node, palette, isDark);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================
|
|
211
|
+
// Edge path generator
|
|
212
|
+
// ============================================================
|
|
213
|
+
|
|
214
|
+
const lineGenerator = d3Shape.line<{ x: number; y: number }>()
|
|
215
|
+
.x((d) => d.x)
|
|
216
|
+
.y((d) => d.y)
|
|
217
|
+
.curve(d3Shape.curveBasis);
|
|
218
|
+
|
|
219
|
+
// ============================================================
|
|
220
|
+
// Main renderer
|
|
221
|
+
// ============================================================
|
|
222
|
+
|
|
223
|
+
export function renderFlowchart(
|
|
224
|
+
container: HTMLDivElement,
|
|
225
|
+
graph: ParsedGraph,
|
|
226
|
+
layout: LayoutResult,
|
|
227
|
+
palette: PaletteColors,
|
|
228
|
+
isDark: boolean,
|
|
229
|
+
onClickItem?: (lineNumber: number) => void,
|
|
230
|
+
exportDims?: { width?: number; height?: number }
|
|
231
|
+
): void {
|
|
232
|
+
// Clear existing content (preserve tooltips)
|
|
233
|
+
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
234
|
+
|
|
235
|
+
const width = exportDims?.width ?? container.clientWidth;
|
|
236
|
+
const height = exportDims?.height ?? container.clientHeight;
|
|
237
|
+
if (width <= 0 || height <= 0) return;
|
|
238
|
+
|
|
239
|
+
const titleOffset = graph.title ? TITLE_HEIGHT : 0;
|
|
240
|
+
|
|
241
|
+
// Compute scale to fit diagram in viewport
|
|
242
|
+
const diagramW = layout.width;
|
|
243
|
+
const diagramH = layout.height + titleOffset;
|
|
244
|
+
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
245
|
+
const scaleY = (height - DIAGRAM_PADDING * 2) / diagramH;
|
|
246
|
+
const scale = Math.min(1, scaleX, scaleY);
|
|
247
|
+
|
|
248
|
+
// Center the diagram
|
|
249
|
+
const scaledW = diagramW * scale;
|
|
250
|
+
const scaledH = diagramH * scale;
|
|
251
|
+
const offsetX = (width - scaledW) / 2;
|
|
252
|
+
const offsetY = (height - scaledH) / 2;
|
|
253
|
+
|
|
254
|
+
// Create SVG
|
|
255
|
+
const svg = d3Selection
|
|
256
|
+
.select(container)
|
|
257
|
+
.append('svg')
|
|
258
|
+
.attr('width', width)
|
|
259
|
+
.attr('height', height)
|
|
260
|
+
.style('background', palette.bg)
|
|
261
|
+
.style('font-family', FONT_FAMILY);
|
|
262
|
+
|
|
263
|
+
// Defs: arrowhead markers
|
|
264
|
+
const defs = svg.append('defs');
|
|
265
|
+
|
|
266
|
+
// Default arrowhead
|
|
267
|
+
defs
|
|
268
|
+
.append('marker')
|
|
269
|
+
.attr('id', 'fc-arrow')
|
|
270
|
+
.attr('viewBox', `0 0 ${ARROWHEAD_W} ${ARROWHEAD_H}`)
|
|
271
|
+
.attr('refX', ARROWHEAD_W)
|
|
272
|
+
.attr('refY', ARROWHEAD_H / 2)
|
|
273
|
+
.attr('markerWidth', ARROWHEAD_W)
|
|
274
|
+
.attr('markerHeight', ARROWHEAD_H)
|
|
275
|
+
.attr('orient', 'auto')
|
|
276
|
+
.append('polygon')
|
|
277
|
+
.attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
|
|
278
|
+
.attr('fill', palette.textMuted);
|
|
279
|
+
|
|
280
|
+
// Collect unique edge colors for custom markers
|
|
281
|
+
const edgeColors = new Set<string>();
|
|
282
|
+
for (const edge of layout.edges) {
|
|
283
|
+
if (edge.color) edgeColors.add(edge.color);
|
|
284
|
+
}
|
|
285
|
+
for (const color of edgeColors) {
|
|
286
|
+
const id = `fc-arrow-${color.replace('#', '')}`;
|
|
287
|
+
defs
|
|
288
|
+
.append('marker')
|
|
289
|
+
.attr('id', id)
|
|
290
|
+
.attr('viewBox', `0 0 ${ARROWHEAD_W} ${ARROWHEAD_H}`)
|
|
291
|
+
.attr('refX', ARROWHEAD_W)
|
|
292
|
+
.attr('refY', ARROWHEAD_H / 2)
|
|
293
|
+
.attr('markerWidth', ARROWHEAD_W)
|
|
294
|
+
.attr('markerHeight', ARROWHEAD_H)
|
|
295
|
+
.attr('orient', 'auto')
|
|
296
|
+
.append('polygon')
|
|
297
|
+
.attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
|
|
298
|
+
.attr('fill', color);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Main content group with scale/translate
|
|
302
|
+
const mainG = svg
|
|
303
|
+
.append('g')
|
|
304
|
+
.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
|
|
305
|
+
|
|
306
|
+
// Title
|
|
307
|
+
if (graph.title) {
|
|
308
|
+
mainG
|
|
309
|
+
.append('text')
|
|
310
|
+
.attr('x', diagramW / 2)
|
|
311
|
+
.attr('y', TITLE_FONT_SIZE)
|
|
312
|
+
.attr('text-anchor', 'middle')
|
|
313
|
+
.attr('fill', palette.text)
|
|
314
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
315
|
+
.attr('font-weight', 'bold')
|
|
316
|
+
.attr('class', 'fc-title')
|
|
317
|
+
.text(graph.title);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Content group (offset by title)
|
|
321
|
+
const contentG = mainG
|
|
322
|
+
.append('g')
|
|
323
|
+
.attr('transform', `translate(0, ${titleOffset})`);
|
|
324
|
+
|
|
325
|
+
// Render groups (background layer)
|
|
326
|
+
for (const group of layout.groups) {
|
|
327
|
+
if (group.width === 0 && group.height === 0) continue;
|
|
328
|
+
const gx = group.x - GROUP_EXTRA_PADDING;
|
|
329
|
+
const gy = group.y - GROUP_EXTRA_PADDING - GROUP_LABEL_FONT_SIZE - 4;
|
|
330
|
+
const gw = group.width + GROUP_EXTRA_PADDING * 2;
|
|
331
|
+
const gh = group.height + GROUP_EXTRA_PADDING * 2 + GROUP_LABEL_FONT_SIZE + 4;
|
|
332
|
+
|
|
333
|
+
const fillColor = group.color
|
|
334
|
+
? mix(group.color, isDark ? palette.surface : palette.bg, 10)
|
|
335
|
+
: isDark
|
|
336
|
+
? palette.surface
|
|
337
|
+
: mix(palette.border, palette.bg, 30);
|
|
338
|
+
const strokeColor = group.color ?? palette.textMuted;
|
|
339
|
+
|
|
340
|
+
contentG
|
|
341
|
+
.append('rect')
|
|
342
|
+
.attr('x', gx)
|
|
343
|
+
.attr('y', gy)
|
|
344
|
+
.attr('width', gw)
|
|
345
|
+
.attr('height', gh)
|
|
346
|
+
.attr('rx', 6)
|
|
347
|
+
.attr('fill', fillColor)
|
|
348
|
+
.attr('stroke', strokeColor)
|
|
349
|
+
.attr('stroke-width', 1)
|
|
350
|
+
.attr('stroke-opacity', 0.5)
|
|
351
|
+
.attr('class', 'fc-group');
|
|
352
|
+
|
|
353
|
+
contentG
|
|
354
|
+
.append('text')
|
|
355
|
+
.attr('x', gx + 8)
|
|
356
|
+
.attr('y', gy + GROUP_LABEL_FONT_SIZE + 4)
|
|
357
|
+
.attr('fill', strokeColor)
|
|
358
|
+
.attr('font-size', GROUP_LABEL_FONT_SIZE)
|
|
359
|
+
.attr('font-weight', 'bold')
|
|
360
|
+
.attr('opacity', 0.7)
|
|
361
|
+
.attr('class', 'fc-group-label')
|
|
362
|
+
.text(group.label);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Render edges (middle layer)
|
|
366
|
+
for (const edge of layout.edges) {
|
|
367
|
+
if (edge.points.length < 2) continue;
|
|
368
|
+
const edgeG = contentG
|
|
369
|
+
.append('g')
|
|
370
|
+
.attr('class', 'fc-edge-group')
|
|
371
|
+
.attr('data-line-number', String(edge.lineNumber));
|
|
372
|
+
|
|
373
|
+
const edgeColor = edge.color ?? palette.textMuted;
|
|
374
|
+
const markerId = edge.color
|
|
375
|
+
? `fc-arrow-${edge.color.replace('#', '')}`
|
|
376
|
+
: 'fc-arrow';
|
|
377
|
+
|
|
378
|
+
const pathD = lineGenerator(edge.points);
|
|
379
|
+
if (pathD) {
|
|
380
|
+
edgeG
|
|
381
|
+
.append('path')
|
|
382
|
+
.attr('d', pathD)
|
|
383
|
+
.attr('fill', 'none')
|
|
384
|
+
.attr('stroke', edgeColor)
|
|
385
|
+
.attr('stroke-width', EDGE_STROKE_WIDTH)
|
|
386
|
+
.attr('marker-end', `url(#${markerId})`)
|
|
387
|
+
.attr('class', 'fc-edge');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Edge label at midpoint
|
|
391
|
+
if (edge.label) {
|
|
392
|
+
const midIdx = Math.floor(edge.points.length / 2);
|
|
393
|
+
const midPt = edge.points[midIdx];
|
|
394
|
+
|
|
395
|
+
// Background rect for legibility
|
|
396
|
+
const labelLen = edge.label.length;
|
|
397
|
+
const bgW = labelLen * 7 + 8;
|
|
398
|
+
const bgH = 16;
|
|
399
|
+
edgeG
|
|
400
|
+
.append('rect')
|
|
401
|
+
.attr('x', midPt.x - bgW / 2)
|
|
402
|
+
.attr('y', midPt.y - bgH / 2 - 1)
|
|
403
|
+
.attr('width', bgW)
|
|
404
|
+
.attr('height', bgH)
|
|
405
|
+
.attr('rx', 3)
|
|
406
|
+
.attr('fill', palette.bg)
|
|
407
|
+
.attr('opacity', 0.85)
|
|
408
|
+
.attr('class', 'fc-edge-label-bg');
|
|
409
|
+
|
|
410
|
+
edgeG
|
|
411
|
+
.append('text')
|
|
412
|
+
.attr('x', midPt.x)
|
|
413
|
+
.attr('y', midPt.y + 4)
|
|
414
|
+
.attr('text-anchor', 'middle')
|
|
415
|
+
.attr('fill', edgeColor)
|
|
416
|
+
.attr('font-size', EDGE_LABEL_FONT_SIZE)
|
|
417
|
+
.attr('class', 'fc-edge-label')
|
|
418
|
+
.text(edge.label);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Render nodes (top layer)
|
|
423
|
+
for (const node of layout.nodes) {
|
|
424
|
+
const nodeG = contentG
|
|
425
|
+
.append('g')
|
|
426
|
+
.attr('transform', `translate(${node.x}, ${node.y})`)
|
|
427
|
+
.attr('class', 'fc-node')
|
|
428
|
+
.attr('data-line-number', String(node.lineNumber));
|
|
429
|
+
|
|
430
|
+
if (onClickItem) {
|
|
431
|
+
nodeG.style('cursor', 'pointer').on('click', () => {
|
|
432
|
+
onClickItem(node.lineNumber);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Shape
|
|
437
|
+
renderNodeShape(nodeG as GSelection, node, palette, isDark);
|
|
438
|
+
|
|
439
|
+
// Label
|
|
440
|
+
nodeG
|
|
441
|
+
.append('text')
|
|
442
|
+
.attr('x', 0)
|
|
443
|
+
.attr('y', 0)
|
|
444
|
+
.attr('text-anchor', 'middle')
|
|
445
|
+
.attr('dominant-baseline', 'central')
|
|
446
|
+
.attr('fill', palette.text)
|
|
447
|
+
.attr('font-size', NODE_FONT_SIZE)
|
|
448
|
+
.text(node.label);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============================================================
|
|
453
|
+
// Export convenience function
|
|
454
|
+
// ============================================================
|
|
455
|
+
|
|
456
|
+
export function renderFlowchartForExport(
|
|
457
|
+
content: string,
|
|
458
|
+
theme: 'light' | 'dark' | 'transparent',
|
|
459
|
+
palette: PaletteColors
|
|
460
|
+
): string {
|
|
461
|
+
const parsed = parseFlowchart(content, palette);
|
|
462
|
+
if (parsed.error || parsed.nodes.length === 0) return '';
|
|
463
|
+
|
|
464
|
+
const layout = layoutGraph(parsed);
|
|
465
|
+
const isDark = theme === 'dark';
|
|
466
|
+
|
|
467
|
+
// Create offscreen container
|
|
468
|
+
const container = document.createElement('div');
|
|
469
|
+
container.style.width = `${layout.width + DIAGRAM_PADDING * 2}px`;
|
|
470
|
+
container.style.height = `${layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? TITLE_HEIGHT : 0)}px`;
|
|
471
|
+
container.style.position = 'absolute';
|
|
472
|
+
container.style.left = '-9999px';
|
|
473
|
+
document.body.appendChild(container);
|
|
474
|
+
|
|
475
|
+
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
476
|
+
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? TITLE_HEIGHT : 0);
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
renderFlowchart(
|
|
480
|
+
container,
|
|
481
|
+
parsed,
|
|
482
|
+
layout,
|
|
483
|
+
palette,
|
|
484
|
+
isDark,
|
|
485
|
+
undefined,
|
|
486
|
+
{ width: exportWidth, height: exportHeight }
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
const svgEl = container.querySelector('svg');
|
|
490
|
+
if (!svgEl) return '';
|
|
491
|
+
|
|
492
|
+
if (theme === 'transparent') {
|
|
493
|
+
svgEl.style.background = 'none';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
497
|
+
svgEl.style.fontFamily = FONT_FAMILY;
|
|
498
|
+
|
|
499
|
+
return svgEl.outerHTML;
|
|
500
|
+
} finally {
|
|
501
|
+
document.body.removeChild(container);
|
|
502
|
+
}
|
|
503
|
+
}
|