@diagrammo/dgmo 0.4.1 → 0.4.3
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/.claude/skills/dgmo-chart/SKILL.md +28 -0
- package/.claude/skills/dgmo-generate/SKILL.md +1 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +24 -1
- package/.cursorrules +27 -2
- package/.github/copilot-instructions.md +36 -3
- package/.windsurfrules +27 -2
- package/README.md +12 -3
- package/dist/cli.cjs +611 -153
- package/dist/index.cjs +8371 -3200
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +502 -58
- package/dist/index.d.ts +502 -58
- package/dist/index.js +8594 -3444
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +336 -17
- package/docs/migration-sequence-color-to-tags.md +98 -0
- package/package.json +1 -1
- package/src/c4/renderer.ts +1 -20
- package/src/class/renderer.ts +1 -11
- package/src/cli.ts +40 -0
- package/src/d3.ts +92 -2
- package/src/dgmo-router.ts +11 -0
- package/src/echarts.ts +74 -8
- package/src/er/parser.ts +29 -3
- package/src/er/renderer.ts +1 -15
- package/src/graph/flowchart-parser.ts +7 -30
- package/src/graph/flowchart-renderer.ts +62 -69
- package/src/graph/layout.ts +5 -0
- package/src/graph/state-parser.ts +388 -0
- package/src/graph/state-renderer.ts +496 -0
- package/src/graph/types.ts +4 -2
- package/src/index.ts +42 -1
- package/src/infra/compute.ts +1113 -0
- package/src/infra/layout.ts +575 -0
- package/src/infra/parser.ts +559 -0
- package/src/infra/renderer.ts +1509 -0
- package/src/infra/roles.ts +60 -0
- package/src/infra/serialize.ts +67 -0
- package/src/infra/types.ts +221 -0
- package/src/infra/validation.ts +192 -0
- package/src/initiative-status/layout.ts +56 -61
- package/src/initiative-status/renderer.ts +13 -13
- package/src/kanban/renderer.ts +1 -24
- package/src/org/layout.ts +28 -37
- package/src/org/parser.ts +16 -1
- package/src/org/renderer.ts +159 -121
- package/src/org/resolver.ts +90 -23
- package/src/palettes/color-utils.ts +30 -0
- package/src/render.ts +2 -0
- package/src/sequence/parser.ts +202 -42
- package/src/sequence/renderer.ts +576 -113
- package/src/sequence/tag-resolution.ts +163 -0
- package/src/sitemap/collapse.ts +187 -0
- package/src/sitemap/layout.ts +738 -0
- package/src/sitemap/parser.ts +489 -0
- package/src/sitemap/renderer.ts +774 -0
- package/src/sitemap/types.ts +42 -0
- package/src/utils/tag-groups.ts +119 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// State Diagram 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 { mix } from '../palettes/color-utils';
|
|
10
|
+
import type { ParsedGraph } from './types';
|
|
11
|
+
import type { LayoutResult, LayoutNode, LayoutEdge } from './layout';
|
|
12
|
+
import { parseState } from './state-parser';
|
|
13
|
+
import { layoutGraph } from './layout';
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// Constants
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
const DIAGRAM_PADDING = 20;
|
|
20
|
+
const MAX_SCALE = 3;
|
|
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 PSEUDOSTATE_RADIUS = 10;
|
|
29
|
+
const STATE_CORNER_RADIUS = 10;
|
|
30
|
+
const GROUP_EXTRA_PADDING = 12;
|
|
31
|
+
|
|
32
|
+
// ============================================================
|
|
33
|
+
// Color helpers
|
|
34
|
+
// ============================================================
|
|
35
|
+
|
|
36
|
+
function stateDefaultColor(palette: PaletteColors, colorOff?: boolean): string {
|
|
37
|
+
return colorOff ? palette.textMuted : palette.colors.blue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stateFill(palette: PaletteColors, isDark: boolean, nodeColor?: string, colorOff?: boolean): string {
|
|
41
|
+
const color = nodeColor ?? stateDefaultColor(palette, colorOff);
|
|
42
|
+
return mix(color, isDark ? palette.surface : palette.bg, 25);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stateStroke(palette: PaletteColors, nodeColor?: string, colorOff?: boolean): string {
|
|
46
|
+
return nodeColor ?? stateDefaultColor(palette, colorOff);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================
|
|
50
|
+
// Edge path generator
|
|
51
|
+
// ============================================================
|
|
52
|
+
|
|
53
|
+
const lineGenerator = d3Shape.line<{ x: number; y: number }>()
|
|
54
|
+
.x((d) => d.x)
|
|
55
|
+
.y((d) => d.y)
|
|
56
|
+
.curve(d3Shape.curveBasis);
|
|
57
|
+
|
|
58
|
+
// ============================================================
|
|
59
|
+
// Self-loop path
|
|
60
|
+
// ============================================================
|
|
61
|
+
|
|
62
|
+
function selfLoopPath(node: LayoutNode): string {
|
|
63
|
+
const cx = node.x;
|
|
64
|
+
const cy = node.y;
|
|
65
|
+
const r = node.width / 2;
|
|
66
|
+
// Loop from right side, arc above, back to right side
|
|
67
|
+
const startX = cx + r;
|
|
68
|
+
const startY = cy - 5;
|
|
69
|
+
const endX = cx + r;
|
|
70
|
+
const endY = cy + 5;
|
|
71
|
+
const loopR = 25;
|
|
72
|
+
return `M ${startX} ${startY} C ${startX + loopR * 2} ${startY - loopR * 2}, ${endX + loopR * 2} ${endY + loopR * 2}, ${endX} ${endY}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================
|
|
76
|
+
// Main renderer
|
|
77
|
+
// ============================================================
|
|
78
|
+
|
|
79
|
+
type GSelection = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
80
|
+
|
|
81
|
+
export function renderState(
|
|
82
|
+
container: HTMLDivElement,
|
|
83
|
+
graph: ParsedGraph,
|
|
84
|
+
layout: LayoutResult,
|
|
85
|
+
palette: PaletteColors,
|
|
86
|
+
isDark: boolean,
|
|
87
|
+
onClickItem?: (lineNumber: number) => void,
|
|
88
|
+
exportDims?: { width?: number; height?: number }
|
|
89
|
+
): void {
|
|
90
|
+
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
91
|
+
|
|
92
|
+
const width = exportDims?.width ?? container.clientWidth;
|
|
93
|
+
const height = exportDims?.height ?? container.clientHeight;
|
|
94
|
+
if (width <= 0 || height <= 0) return;
|
|
95
|
+
|
|
96
|
+
const titleHeight = graph.title ? 40 : 0;
|
|
97
|
+
|
|
98
|
+
const diagramW = layout.width;
|
|
99
|
+
const diagramH = layout.height;
|
|
100
|
+
const availH = height - titleHeight;
|
|
101
|
+
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
102
|
+
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
103
|
+
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
104
|
+
|
|
105
|
+
const scaledW = diagramW * scale;
|
|
106
|
+
const offsetX = (width - scaledW) / 2;
|
|
107
|
+
const offsetY = titleHeight + DIAGRAM_PADDING;
|
|
108
|
+
|
|
109
|
+
const svg = d3Selection
|
|
110
|
+
.select(container)
|
|
111
|
+
.append('svg')
|
|
112
|
+
.attr('width', width)
|
|
113
|
+
.attr('height', height)
|
|
114
|
+
.style('font-family', FONT_FAMILY);
|
|
115
|
+
|
|
116
|
+
// Defs: arrowhead markers
|
|
117
|
+
const defs = svg.append('defs');
|
|
118
|
+
|
|
119
|
+
defs
|
|
120
|
+
.append('marker')
|
|
121
|
+
.attr('id', 'st-arrow')
|
|
122
|
+
.attr('viewBox', `0 0 ${ARROWHEAD_W} ${ARROWHEAD_H}`)
|
|
123
|
+
.attr('refX', ARROWHEAD_W)
|
|
124
|
+
.attr('refY', ARROWHEAD_H / 2)
|
|
125
|
+
.attr('markerWidth', ARROWHEAD_W)
|
|
126
|
+
.attr('markerHeight', ARROWHEAD_H)
|
|
127
|
+
.attr('orient', 'auto')
|
|
128
|
+
.append('polygon')
|
|
129
|
+
.attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
|
|
130
|
+
.attr('fill', palette.textMuted);
|
|
131
|
+
|
|
132
|
+
// Custom colored markers
|
|
133
|
+
const edgeColors = new Set<string>();
|
|
134
|
+
for (const edge of layout.edges) {
|
|
135
|
+
if (edge.color) edgeColors.add(edge.color);
|
|
136
|
+
}
|
|
137
|
+
for (const color of edgeColors) {
|
|
138
|
+
const id = `st-arrow-${color.replace('#', '')}`;
|
|
139
|
+
defs
|
|
140
|
+
.append('marker')
|
|
141
|
+
.attr('id', id)
|
|
142
|
+
.attr('viewBox', `0 0 ${ARROWHEAD_W} ${ARROWHEAD_H}`)
|
|
143
|
+
.attr('refX', ARROWHEAD_W)
|
|
144
|
+
.attr('refY', ARROWHEAD_H / 2)
|
|
145
|
+
.attr('markerWidth', ARROWHEAD_W)
|
|
146
|
+
.attr('markerHeight', ARROWHEAD_H)
|
|
147
|
+
.attr('orient', 'auto')
|
|
148
|
+
.append('polygon')
|
|
149
|
+
.attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
|
|
150
|
+
.attr('fill', color);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Title
|
|
154
|
+
if (graph.title) {
|
|
155
|
+
const titleEl = svg
|
|
156
|
+
.append('text')
|
|
157
|
+
.attr('class', 'chart-title')
|
|
158
|
+
.attr('x', width / 2)
|
|
159
|
+
.attr('y', 30)
|
|
160
|
+
.attr('text-anchor', 'middle')
|
|
161
|
+
.attr('fill', palette.text)
|
|
162
|
+
.attr('font-size', '20px')
|
|
163
|
+
.attr('font-weight', '700')
|
|
164
|
+
.style('cursor', onClickItem && graph.titleLineNumber ? 'pointer' : 'default')
|
|
165
|
+
.text(graph.title);
|
|
166
|
+
|
|
167
|
+
if (graph.titleLineNumber) {
|
|
168
|
+
titleEl.attr('data-line-number', graph.titleLineNumber);
|
|
169
|
+
if (onClickItem) {
|
|
170
|
+
titleEl
|
|
171
|
+
.on('click', () => onClickItem(graph.titleLineNumber!))
|
|
172
|
+
.on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
|
|
173
|
+
.on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Content group
|
|
179
|
+
const contentG = svg
|
|
180
|
+
.append('g')
|
|
181
|
+
.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
|
|
182
|
+
|
|
183
|
+
// Render groups (background layer)
|
|
184
|
+
for (const group of layout.groups) {
|
|
185
|
+
if (group.width === 0 && group.height === 0) continue;
|
|
186
|
+
const gx = group.x - GROUP_EXTRA_PADDING;
|
|
187
|
+
const gy = group.y - GROUP_EXTRA_PADDING - GROUP_LABEL_FONT_SIZE - 4;
|
|
188
|
+
const gw = group.width + GROUP_EXTRA_PADDING * 2;
|
|
189
|
+
const gh = group.height + GROUP_EXTRA_PADDING * 2 + GROUP_LABEL_FONT_SIZE + 4;
|
|
190
|
+
|
|
191
|
+
const fillColor = group.color
|
|
192
|
+
? mix(group.color, isDark ? palette.surface : palette.bg, 10)
|
|
193
|
+
: isDark
|
|
194
|
+
? palette.surface
|
|
195
|
+
: mix(palette.border, palette.bg, 30);
|
|
196
|
+
const strokeColor = group.color ?? palette.textMuted;
|
|
197
|
+
|
|
198
|
+
const groupWrapper = contentG
|
|
199
|
+
.append('g')
|
|
200
|
+
.attr('class', 'st-group-wrapper')
|
|
201
|
+
.attr('data-line-number', String(group.lineNumber))
|
|
202
|
+
.attr('data-group-id', group.id);
|
|
203
|
+
|
|
204
|
+
if (onClickItem) {
|
|
205
|
+
groupWrapper.style('cursor', 'pointer').on('click', () => {
|
|
206
|
+
onClickItem(group.lineNumber);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
groupWrapper
|
|
211
|
+
.append('rect')
|
|
212
|
+
.attr('x', gx)
|
|
213
|
+
.attr('y', gy)
|
|
214
|
+
.attr('width', gw)
|
|
215
|
+
.attr('height', gh)
|
|
216
|
+
.attr('rx', 6)
|
|
217
|
+
.attr('fill', fillColor)
|
|
218
|
+
.attr('stroke', strokeColor)
|
|
219
|
+
.attr('stroke-width', 1)
|
|
220
|
+
.attr('stroke-opacity', 0.5)
|
|
221
|
+
.attr('class', 'st-group');
|
|
222
|
+
|
|
223
|
+
groupWrapper
|
|
224
|
+
.append('text')
|
|
225
|
+
.attr('x', gx + 8)
|
|
226
|
+
.attr('y', gy + GROUP_LABEL_FONT_SIZE + 4)
|
|
227
|
+
.attr('fill', strokeColor)
|
|
228
|
+
.attr('font-size', GROUP_LABEL_FONT_SIZE)
|
|
229
|
+
.attr('font-weight', 'bold')
|
|
230
|
+
.attr('opacity', 0.7)
|
|
231
|
+
.attr('class', 'st-group-label')
|
|
232
|
+
.text(group.label);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Build self-loop lookup
|
|
236
|
+
const selfLoopEdges = new Set<number>();
|
|
237
|
+
for (const edge of layout.edges) {
|
|
238
|
+
if (edge.source === edge.target) selfLoopEdges.add(edge.lineNumber);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Build node position map for self-loops
|
|
242
|
+
const nodePositionMap = new Map<string, LayoutNode>();
|
|
243
|
+
for (const node of layout.nodes) {
|
|
244
|
+
nodePositionMap.set(node.id, node);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Compute edge label positions with perpendicular offset to hug their path,
|
|
248
|
+
// then resolve remaining collisions.
|
|
249
|
+
const LABEL_CHAR_W = 7;
|
|
250
|
+
const LABEL_PAD = 8;
|
|
251
|
+
const LABEL_H = 16;
|
|
252
|
+
const PERP_OFFSET = 10; // px offset perpendicular to edge direction
|
|
253
|
+
|
|
254
|
+
interface LabelPos { x: number; y: number; w: number; h: number; edgeIdx: number }
|
|
255
|
+
const labelPositions: LabelPos[] = [];
|
|
256
|
+
|
|
257
|
+
for (let ei = 0; ei < layout.edges.length; ei++) {
|
|
258
|
+
const edge = layout.edges[ei];
|
|
259
|
+
if (!edge.label) continue;
|
|
260
|
+
const bgW = edge.label.length * LABEL_CHAR_W + LABEL_PAD;
|
|
261
|
+
let lx: number, ly: number;
|
|
262
|
+
|
|
263
|
+
if (edge.source === edge.target) {
|
|
264
|
+
const node = nodePositionMap.get(edge.source);
|
|
265
|
+
if (!node) continue;
|
|
266
|
+
lx = node.x + node.width / 2 + 30;
|
|
267
|
+
ly = node.y;
|
|
268
|
+
} else if (edge.points.length >= 2) {
|
|
269
|
+
const midIdx = Math.floor(edge.points.length / 2);
|
|
270
|
+
const midPt = edge.points[midIdx];
|
|
271
|
+
// Compute perpendicular offset from edge direction at midpoint
|
|
272
|
+
const prev = edge.points[Math.max(0, midIdx - 1)];
|
|
273
|
+
const next = edge.points[Math.min(edge.points.length - 1, midIdx + 1)];
|
|
274
|
+
const dx = next.x - prev.x;
|
|
275
|
+
const dy = next.y - prev.y;
|
|
276
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
277
|
+
if (len > 0) {
|
|
278
|
+
// Normal vector (right-hand side of travel direction)
|
|
279
|
+
lx = midPt.x + (-dy / len) * PERP_OFFSET;
|
|
280
|
+
ly = midPt.y + (dx / len) * PERP_OFFSET;
|
|
281
|
+
} else {
|
|
282
|
+
lx = midPt.x;
|
|
283
|
+
ly = midPt.y;
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
labelPositions.push({ x: lx, y: ly, w: bgW, h: LABEL_H, edgeIdx: ei });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Resolve remaining label collisions: nudge overlapping labels apart.
|
|
292
|
+
labelPositions.sort((a, b) => a.y - b.y);
|
|
293
|
+
for (let i = 0; i < labelPositions.length; i++) {
|
|
294
|
+
for (let j = i + 1; j < labelPositions.length; j++) {
|
|
295
|
+
const a = labelPositions[i];
|
|
296
|
+
const b = labelPositions[j];
|
|
297
|
+
const overlapX = Math.abs(a.x - b.x) < (a.w + b.w) / 2;
|
|
298
|
+
const overlapY = Math.abs(a.y - b.y) < (a.h + b.h) / 2;
|
|
299
|
+
if (overlapX && overlapY) {
|
|
300
|
+
b.y = a.y + (a.h + b.h) / 2 + 2;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Build lookup: edgeIdx → adjusted label position
|
|
306
|
+
const labelPosMap = new Map<number, LabelPos>();
|
|
307
|
+
for (const lp of labelPositions) labelPosMap.set(lp.edgeIdx, lp);
|
|
308
|
+
|
|
309
|
+
// Render edges (middle layer)
|
|
310
|
+
for (let ei = 0; ei < layout.edges.length; ei++) {
|
|
311
|
+
const edge = layout.edges[ei];
|
|
312
|
+
const edgeG = contentG
|
|
313
|
+
.append('g')
|
|
314
|
+
.attr('class', 'st-edge-group')
|
|
315
|
+
.attr('data-line-number', String(edge.lineNumber));
|
|
316
|
+
|
|
317
|
+
const edgeColor = edge.color ?? palette.textMuted;
|
|
318
|
+
const markerId = edge.color
|
|
319
|
+
? `st-arrow-${edge.color.replace('#', '')}`
|
|
320
|
+
: 'st-arrow';
|
|
321
|
+
|
|
322
|
+
if (edge.source === edge.target) {
|
|
323
|
+
// Self-loop
|
|
324
|
+
const node = nodePositionMap.get(edge.source);
|
|
325
|
+
if (node) {
|
|
326
|
+
edgeG
|
|
327
|
+
.append('path')
|
|
328
|
+
.attr('d', selfLoopPath(node))
|
|
329
|
+
.attr('fill', 'none')
|
|
330
|
+
.attr('stroke', edgeColor)
|
|
331
|
+
.attr('stroke-width', EDGE_STROKE_WIDTH)
|
|
332
|
+
.attr('marker-end', `url(#${markerId})`)
|
|
333
|
+
.attr('class', 'st-edge');
|
|
334
|
+
|
|
335
|
+
const lp = labelPosMap.get(ei);
|
|
336
|
+
if (edge.label && lp) {
|
|
337
|
+
edgeG.append('rect')
|
|
338
|
+
.attr('x', lp.x - lp.w / 2)
|
|
339
|
+
.attr('y', lp.y - lp.h / 2 - 1)
|
|
340
|
+
.attr('width', lp.w)
|
|
341
|
+
.attr('height', lp.h)
|
|
342
|
+
.attr('rx', 3)
|
|
343
|
+
.attr('fill', palette.bg)
|
|
344
|
+
.attr('opacity', 0.85)
|
|
345
|
+
.attr('class', 'st-edge-label-bg');
|
|
346
|
+
edgeG.append('text')
|
|
347
|
+
.attr('x', lp.x)
|
|
348
|
+
.attr('y', lp.y + 4)
|
|
349
|
+
.attr('text-anchor', 'middle')
|
|
350
|
+
.attr('fill', edgeColor)
|
|
351
|
+
.attr('font-size', EDGE_LABEL_FONT_SIZE)
|
|
352
|
+
.attr('class', 'st-edge-label')
|
|
353
|
+
.text(edge.label);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} else if (edge.points.length >= 2) {
|
|
357
|
+
const pathD = lineGenerator(edge.points);
|
|
358
|
+
if (pathD) {
|
|
359
|
+
edgeG
|
|
360
|
+
.append('path')
|
|
361
|
+
.attr('d', pathD)
|
|
362
|
+
.attr('fill', 'none')
|
|
363
|
+
.attr('stroke', edgeColor)
|
|
364
|
+
.attr('stroke-width', EDGE_STROKE_WIDTH)
|
|
365
|
+
.attr('marker-end', `url(#${markerId})`)
|
|
366
|
+
.attr('class', 'st-edge');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const lp = labelPosMap.get(ei);
|
|
370
|
+
if (edge.label && lp) {
|
|
371
|
+
edgeG.append('rect')
|
|
372
|
+
.attr('x', lp.x - lp.w / 2)
|
|
373
|
+
.attr('y', lp.y - lp.h / 2 - 1)
|
|
374
|
+
.attr('width', lp.w)
|
|
375
|
+
.attr('height', lp.h)
|
|
376
|
+
.attr('rx', 3)
|
|
377
|
+
.attr('fill', palette.bg)
|
|
378
|
+
.attr('opacity', 0.85)
|
|
379
|
+
.attr('class', 'st-edge-label-bg');
|
|
380
|
+
edgeG.append('text')
|
|
381
|
+
.attr('x', lp.x)
|
|
382
|
+
.attr('y', lp.y + 4)
|
|
383
|
+
.attr('text-anchor', 'middle')
|
|
384
|
+
.attr('fill', edgeColor)
|
|
385
|
+
.attr('font-size', EDGE_LABEL_FONT_SIZE)
|
|
386
|
+
.attr('class', 'st-edge-label')
|
|
387
|
+
.text(edge.label);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Render nodes (top layer)
|
|
393
|
+
const colorOff = graph.options?.color === 'off';
|
|
394
|
+
for (const node of layout.nodes) {
|
|
395
|
+
const nodeG = contentG
|
|
396
|
+
.append('g')
|
|
397
|
+
.attr('transform', `translate(${node.x}, ${node.y})`)
|
|
398
|
+
.attr('class', 'st-node')
|
|
399
|
+
.attr('data-line-number', String(node.lineNumber))
|
|
400
|
+
.attr('data-node-id', node.id);
|
|
401
|
+
|
|
402
|
+
if (onClickItem) {
|
|
403
|
+
nodeG.style('cursor', 'pointer').on('click', () => {
|
|
404
|
+
onClickItem(node.lineNumber);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (node.shape === 'pseudostate') {
|
|
409
|
+
// Filled circle
|
|
410
|
+
nodeG
|
|
411
|
+
.append('circle')
|
|
412
|
+
.attr('cx', 0)
|
|
413
|
+
.attr('cy', 0)
|
|
414
|
+
.attr('r', PSEUDOSTATE_RADIUS)
|
|
415
|
+
.attr('fill', palette.text)
|
|
416
|
+
.attr('stroke', 'none');
|
|
417
|
+
} else {
|
|
418
|
+
// State — rounded rectangle
|
|
419
|
+
const w = node.width;
|
|
420
|
+
const h = node.height;
|
|
421
|
+
nodeG
|
|
422
|
+
.append('rect')
|
|
423
|
+
.attr('x', -w / 2)
|
|
424
|
+
.attr('y', -h / 2)
|
|
425
|
+
.attr('width', w)
|
|
426
|
+
.attr('height', h)
|
|
427
|
+
.attr('rx', STATE_CORNER_RADIUS)
|
|
428
|
+
.attr('ry', STATE_CORNER_RADIUS)
|
|
429
|
+
.attr('fill', stateFill(palette, isDark, node.color, colorOff))
|
|
430
|
+
.attr('stroke', stateStroke(palette, node.color, colorOff))
|
|
431
|
+
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
432
|
+
|
|
433
|
+
// Label
|
|
434
|
+
nodeG
|
|
435
|
+
.append('text')
|
|
436
|
+
.attr('x', 0)
|
|
437
|
+
.attr('y', 0)
|
|
438
|
+
.attr('text-anchor', 'middle')
|
|
439
|
+
.attr('dominant-baseline', 'central')
|
|
440
|
+
.attr('fill', palette.text)
|
|
441
|
+
.attr('font-size', NODE_FONT_SIZE)
|
|
442
|
+
.text(node.label);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================================
|
|
448
|
+
// Export convenience function
|
|
449
|
+
// ============================================================
|
|
450
|
+
|
|
451
|
+
export function renderStateForExport(
|
|
452
|
+
content: string,
|
|
453
|
+
theme: 'light' | 'dark' | 'transparent',
|
|
454
|
+
palette: PaletteColors
|
|
455
|
+
): string {
|
|
456
|
+
const parsed = parseState(content, palette);
|
|
457
|
+
if (parsed.error || parsed.nodes.length === 0) return '';
|
|
458
|
+
|
|
459
|
+
const layout = layoutGraph(parsed);
|
|
460
|
+
const isDark = theme === 'dark';
|
|
461
|
+
|
|
462
|
+
const container = document.createElement('div');
|
|
463
|
+
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
464
|
+
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0);
|
|
465
|
+
container.style.width = `${exportWidth}px`;
|
|
466
|
+
container.style.height = `${exportHeight}px`;
|
|
467
|
+
container.style.position = 'absolute';
|
|
468
|
+
container.style.left = '-9999px';
|
|
469
|
+
document.body.appendChild(container);
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
renderState(
|
|
473
|
+
container,
|
|
474
|
+
parsed,
|
|
475
|
+
layout,
|
|
476
|
+
palette,
|
|
477
|
+
isDark,
|
|
478
|
+
undefined,
|
|
479
|
+
{ width: exportWidth, height: exportHeight }
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const svgEl = container.querySelector('svg');
|
|
483
|
+
if (!svgEl) return '';
|
|
484
|
+
|
|
485
|
+
if (theme === 'transparent') {
|
|
486
|
+
svgEl.style.background = 'none';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
490
|
+
svgEl.style.fontFamily = FONT_FAMILY;
|
|
491
|
+
|
|
492
|
+
return svgEl.outerHTML;
|
|
493
|
+
} finally {
|
|
494
|
+
document.body.removeChild(container);
|
|
495
|
+
}
|
|
496
|
+
}
|
package/src/graph/types.ts
CHANGED
|
@@ -4,7 +4,9 @@ export type GraphShape =
|
|
|
4
4
|
| 'decision' // <> — diamond
|
|
5
5
|
| 'io' // // — parallelogram
|
|
6
6
|
| 'subroutine' // [[]] — double-bordered rectangle
|
|
7
|
-
| 'document'
|
|
7
|
+
| 'document' // [~] — wavy-bottom rectangle
|
|
8
|
+
| 'state' // state diagram — rounded rectangle
|
|
9
|
+
| 'pseudostate'; // [*] — filled circle (start/end)
|
|
8
10
|
|
|
9
11
|
export type GraphDirection = 'TB' | 'LR';
|
|
10
12
|
|
|
@@ -36,7 +38,7 @@ export interface GraphGroup {
|
|
|
36
38
|
import type { DgmoError } from '../diagnostics';
|
|
37
39
|
|
|
38
40
|
export interface ParsedGraph {
|
|
39
|
-
type: 'flowchart';
|
|
41
|
+
type: 'flowchart' | 'state';
|
|
40
42
|
title?: string;
|
|
41
43
|
titleLineNumber?: number;
|
|
42
44
|
direction: GraphDirection;
|
package/src/index.ts
CHANGED
|
@@ -77,6 +77,9 @@ export type { ParsedQuadrant } from './dgmo-mermaid';
|
|
|
77
77
|
|
|
78
78
|
export { parseFlowchart, looksLikeFlowchart } from './graph/flowchart-parser';
|
|
79
79
|
|
|
80
|
+
export { parseState, looksLikeState } from './graph/state-parser';
|
|
81
|
+
export { renderState, renderStateForExport } from './graph/state-renderer';
|
|
82
|
+
|
|
80
83
|
export { parseClassDiagram, looksLikeClassDiagram } from './class/parser';
|
|
81
84
|
|
|
82
85
|
export type {
|
|
@@ -215,11 +218,49 @@ export type {
|
|
|
215
218
|
|
|
216
219
|
export { renderInitiativeStatus, renderInitiativeStatusForExport } from './initiative-status/renderer';
|
|
217
220
|
|
|
221
|
+
export { parseSitemap, looksLikeSitemap } from './sitemap/parser';
|
|
222
|
+
|
|
223
|
+
export type {
|
|
224
|
+
ParsedSitemap,
|
|
225
|
+
SitemapNode,
|
|
226
|
+
SitemapEdge,
|
|
227
|
+
SitemapDirection,
|
|
228
|
+
} from './sitemap/types';
|
|
229
|
+
|
|
230
|
+
export { layoutSitemap } from './sitemap/layout';
|
|
231
|
+
export type {
|
|
232
|
+
SitemapLayoutResult,
|
|
233
|
+
SitemapLayoutNode,
|
|
234
|
+
SitemapLayoutEdge,
|
|
235
|
+
SitemapContainerBounds,
|
|
236
|
+
SitemapLegendGroup,
|
|
237
|
+
SitemapLegendEntry,
|
|
238
|
+
} from './sitemap/layout';
|
|
239
|
+
|
|
240
|
+
export { renderSitemap, renderSitemapForExport } from './sitemap/renderer';
|
|
241
|
+
|
|
242
|
+
export { collapseSitemapTree } from './sitemap/collapse';
|
|
243
|
+
|
|
244
|
+
// ── Infra Chart ────────────────────────────────────────────
|
|
245
|
+
export { parseInfra } from './infra/parser';
|
|
246
|
+
export type { ParsedInfra, InfraNode, InfraEdge, InfraGroup, InfraTagGroup, InfraProperty, InfraDiagnostic, InfraScenario, InfraComputeParams, InfraBehaviorKey } from './infra/types';
|
|
247
|
+
export { INFRA_BEHAVIOR_KEYS } from './infra/types';
|
|
248
|
+
export { computeInfra } from './infra/compute';
|
|
249
|
+
export type { ComputedInfraModel, ComputedInfraNode, ComputedInfraEdge, InfraLatencyPercentiles, InfraAvailabilityPercentiles, InfraCbState } from './infra/types';
|
|
250
|
+
export { validateInfra, validateComputed } from './infra/validation';
|
|
251
|
+
export { inferRoles, collectDiagramRoles } from './infra/roles';
|
|
252
|
+
export type { InfraRole } from './infra/roles';
|
|
253
|
+
export { layoutInfra } from './infra/layout';
|
|
254
|
+
export type { InfraLayoutResult, InfraLayoutNode, InfraLayoutEdge, InfraLayoutGroup } from './infra/layout';
|
|
255
|
+
export { renderInfra, parseAndLayoutInfra, computeInfraLegendGroups } from './infra/renderer';
|
|
256
|
+
export type { InfraLegendGroup, InfraPlaybackState } from './infra/renderer';
|
|
257
|
+
export type { CollapsedSitemapResult } from './sitemap/collapse';
|
|
258
|
+
|
|
218
259
|
export { collapseOrgTree } from './org/collapse';
|
|
219
260
|
export type { CollapsedOrgResult } from './org/collapse';
|
|
220
261
|
|
|
221
262
|
export { resolveOrgImports } from './org/resolver';
|
|
222
|
-
export type { ReadFileFn, ResolveImportsResult } from './org/resolver';
|
|
263
|
+
export type { ReadFileFn, ResolveImportsResult, ImportSource } from './org/resolver';
|
|
223
264
|
|
|
224
265
|
export { layoutGraph } from './graph/layout';
|
|
225
266
|
export type {
|