@diagrammo/dgmo 0.8.5 → 0.8.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.
Files changed (65) hide show
  1. package/.claude/commands/dgmo.md +34 -27
  2. package/.cursorrules +20 -2
  3. package/.github/copilot-instructions.md +20 -2
  4. package/.windsurfrules +20 -2
  5. package/AGENTS.md +23 -3
  6. package/README.md +0 -1
  7. package/dist/cli.cjs +189 -190
  8. package/dist/editor.cjs +3 -18
  9. package/dist/editor.cjs.map +1 -1
  10. package/dist/editor.js +3 -18
  11. package/dist/editor.js.map +1 -1
  12. package/dist/highlight.cjs +4 -21
  13. package/dist/highlight.cjs.map +1 -1
  14. package/dist/highlight.js +4 -21
  15. package/dist/highlight.js.map +1 -1
  16. package/dist/index.cjs +2791 -2999
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +56 -56
  19. package/dist/index.d.ts +56 -56
  20. package/dist/index.js +2786 -2992
  21. package/dist/index.js.map +1 -1
  22. package/docs/ai-integration.md +1 -1
  23. package/docs/language-reference.md +112 -121
  24. package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
  25. package/package.json +1 -1
  26. package/src/boxes-and-lines/collapse.ts +78 -0
  27. package/src/boxes-and-lines/layout.ts +319 -0
  28. package/src/boxes-and-lines/parser.ts +697 -0
  29. package/src/boxes-and-lines/renderer.ts +848 -0
  30. package/src/boxes-and-lines/types.ts +40 -0
  31. package/src/c4/parser.ts +10 -5
  32. package/src/c4/renderer.ts +232 -56
  33. package/src/chart.ts +9 -4
  34. package/src/cli.ts +6 -5
  35. package/src/completion.ts +25 -33
  36. package/src/d3.ts +26 -27
  37. package/src/dgmo-router.ts +3 -7
  38. package/src/echarts.ts +38 -2
  39. package/src/editor/keywords.ts +4 -19
  40. package/src/er/parser.ts +10 -4
  41. package/src/gantt/parser.ts +10 -4
  42. package/src/gantt/renderer.ts +3 -5
  43. package/src/index.ts +17 -26
  44. package/src/infra/parser.ts +10 -5
  45. package/src/infra/renderer.ts +2 -2
  46. package/src/kanban/parser.ts +10 -5
  47. package/src/kanban/renderer.ts +43 -18
  48. package/src/org/parser.ts +7 -4
  49. package/src/org/renderer.ts +40 -29
  50. package/src/sequence/parser.ts +11 -5
  51. package/src/sequence/renderer.ts +114 -45
  52. package/src/sitemap/parser.ts +8 -4
  53. package/src/sitemap/renderer.ts +137 -57
  54. package/src/utils/legend-svg.ts +44 -20
  55. package/src/utils/parsing.ts +1 -1
  56. package/src/utils/tag-groups.ts +59 -15
  57. package/gallery/fixtures/initiative-status-full.dgmo +0 -46
  58. package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
  59. package/gallery/fixtures/initiative-status.dgmo +0 -9
  60. package/src/initiative-status/collapse.ts +0 -76
  61. package/src/initiative-status/filter.ts +0 -63
  62. package/src/initiative-status/layout.ts +0 -650
  63. package/src/initiative-status/parser.ts +0 -629
  64. package/src/initiative-status/renderer.ts +0 -1199
  65. package/src/initiative-status/types.ts +0 -57
@@ -0,0 +1,319 @@
1
+ // ============================================================
2
+ // Boxes and Lines Diagram — Layout Engine
3
+ // ============================================================
4
+
5
+ import dagre from '@dagrejs/dagre';
6
+ import type { ParsedBoxesAndLines, BLNode } from './types';
7
+
8
+ // ── Constants ──────────────────────────────────────────────
9
+ const NODESEP = 60;
10
+ const RANKSEP = 100;
11
+ const MARGIN = 40;
12
+ const CONTAINER_PAD_X = 30;
13
+ const CONTAINER_PAD_TOP = 40;
14
+ const CONTAINER_PAD_BOTTOM = 24;
15
+ const MAX_PARALLEL_EDGES = 5;
16
+ const PARALLEL_SPACING = 12;
17
+ const PARALLEL_EDGE_MARGIN = 10;
18
+
19
+ // ── Result types ───────────────────────────────────────────
20
+
21
+ export interface BLLayoutNode {
22
+ label: string;
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ }
28
+
29
+ export interface BLLayoutEdge {
30
+ source: string;
31
+ target: string;
32
+ label?: string;
33
+ bidirectional: boolean;
34
+ lineNumber: number;
35
+ points: { x: number; y: number }[];
36
+ labelX?: number;
37
+ labelY?: number;
38
+ yOffset: number;
39
+ parallelCount: number;
40
+ metadata: Record<string, string>;
41
+ }
42
+
43
+ export interface BLLayoutGroup {
44
+ label: string;
45
+ lineNumber: number;
46
+ x: number;
47
+ y: number;
48
+ width: number;
49
+ height: number;
50
+ collapsed: boolean;
51
+ childCount?: number;
52
+ }
53
+
54
+ export interface BLLayoutResult {
55
+ nodes: BLLayoutNode[];
56
+ edges: BLLayoutEdge[];
57
+ groups: BLLayoutGroup[];
58
+ width: number;
59
+ height: number;
60
+ }
61
+
62
+ // ── Node sizing ────────────────────────────────────────────
63
+
64
+ function computeNodeSize(_node: BLNode): { width: number; height: number } {
65
+ // Golden ratio (φ ≈ 1.618), uniform size
66
+ const PHI = 1.618;
67
+ const NODE_HEIGHT = 60;
68
+ const NODE_WIDTH = Math.round(NODE_HEIGHT * PHI); // ≈ 97
69
+
70
+ return { width: NODE_WIDTH, height: NODE_HEIGHT };
71
+ }
72
+
73
+ // ── Main layout ────────────────────────────────────────────
74
+
75
+ export function layoutBoxesAndLines(
76
+ parsed: ParsedBoxesAndLines,
77
+ collapseInfo?: {
78
+ collapsedChildCounts: Map<string, number>;
79
+ originalGroups: import('./types').BLGroup[];
80
+ }
81
+ ): BLLayoutResult {
82
+ const g = new dagre.graphlib.Graph({ compound: true, multigraph: true });
83
+ g.setGraph({
84
+ rankdir: parsed.direction,
85
+ nodesep: NODESEP,
86
+ ranksep: RANKSEP,
87
+ marginx: MARGIN,
88
+ marginy: MARGIN,
89
+ });
90
+ g.setDefaultEdgeLabel(() => ({}));
91
+
92
+ // Determine which groups are collapsed
93
+ const collapsedGroupLabels = new Set<string>();
94
+ if (collapseInfo) {
95
+ for (const og of collapseInfo.originalGroups) {
96
+ if (!parsed.groups.some((g) => g.label === og.label)) {
97
+ collapsedGroupLabels.add(og.label);
98
+ }
99
+ }
100
+ }
101
+
102
+ // Add collapsed groups as regular nodes — same golden-ratio dimensions
103
+ const PHI = 1.618;
104
+ const COLLAPSED_H = 60;
105
+ const COLLAPSED_W = Math.round(COLLAPSED_H * PHI);
106
+ for (const label of collapsedGroupLabels) {
107
+ const gid = `__group_${label}`;
108
+ g.setNode(gid, { label, width: COLLAPSED_W, height: COLLAPSED_H });
109
+ }
110
+
111
+ // Add expanded group nodes as compound parents
112
+ for (const group of parsed.groups) {
113
+ const gid = `__group_${group.label}`;
114
+ g.setNode(gid, {
115
+ label: group.label,
116
+ paddingLeft: CONTAINER_PAD_X,
117
+ paddingRight: CONTAINER_PAD_X,
118
+ paddingTop: CONTAINER_PAD_TOP,
119
+ paddingBottom: CONTAINER_PAD_BOTTOM,
120
+ });
121
+ }
122
+
123
+ // Add nodes
124
+ for (const node of parsed.nodes) {
125
+ const size = computeNodeSize(node);
126
+ g.setNode(node.label, {
127
+ label: node.label,
128
+ width: size.width,
129
+ height: size.height,
130
+ });
131
+ }
132
+
133
+ // Set parent relationships for nodes in groups
134
+ for (const group of parsed.groups) {
135
+ const gid = `__group_${group.label}`;
136
+ for (const child of group.children) {
137
+ if (g.hasNode(child)) {
138
+ g.setParent(child, gid);
139
+ }
140
+ }
141
+ }
142
+
143
+ // Build set of expanded compound parent IDs (dagre can't handle edges
144
+ // directly on compound parents — they have no rank of their own)
145
+ const expandedGroupIds = new Set<string>();
146
+ for (const group of parsed.groups) {
147
+ expandedGroupIds.add(`__group_${group.label}`);
148
+ }
149
+
150
+ // Add edges — skip edges where either endpoint is an expanded compound parent
151
+ const deferredEdgeIndices: number[] = [];
152
+ for (let i = 0; i < parsed.edges.length; i++) {
153
+ const edge = parsed.edges[i];
154
+ const src = edge.source;
155
+ const tgt = edge.target;
156
+ if (!g.hasNode(src) || !g.hasNode(tgt)) continue;
157
+ if (expandedGroupIds.has(src) || expandedGroupIds.has(tgt)) {
158
+ deferredEdgeIndices.push(i);
159
+ continue;
160
+ }
161
+ g.setEdge(src, tgt, { label: edge.label ?? '', minlen: 1 }, `e${i}`);
162
+ }
163
+
164
+ // Run dagre layout
165
+ dagre.layout(g);
166
+
167
+ // Extract node positions
168
+ const layoutNodes: BLLayoutNode[] = [];
169
+ for (const node of parsed.nodes) {
170
+ const dagreNode = g.node(node.label);
171
+ if (!dagreNode) continue;
172
+ layoutNodes.push({
173
+ label: node.label,
174
+ x: dagreNode.x,
175
+ y: dagreNode.y,
176
+ width: dagreNode.width,
177
+ height: dagreNode.height,
178
+ });
179
+ }
180
+
181
+ // Extract group positions (expanded)
182
+ const layoutGroups: BLLayoutGroup[] = [];
183
+ for (const group of parsed.groups) {
184
+ const gid = `__group_${group.label}`;
185
+ const dagreNode = g.node(gid);
186
+ if (!dagreNode) continue;
187
+ layoutGroups.push({
188
+ label: group.label,
189
+ lineNumber: group.lineNumber,
190
+ x: dagreNode.x,
191
+ y: dagreNode.y,
192
+ width: dagreNode.width,
193
+ height: dagreNode.height,
194
+ collapsed: false,
195
+ });
196
+ }
197
+
198
+ // Extract collapsed group positions
199
+ for (const label of collapsedGroupLabels) {
200
+ const gid = `__group_${label}`;
201
+ const dagreNode = g.node(gid);
202
+ if (!dagreNode) continue;
203
+ const og = collapseInfo?.originalGroups.find((g) => g.label === label);
204
+ layoutGroups.push({
205
+ label,
206
+ lineNumber: og?.lineNumber ?? 0,
207
+ x: dagreNode.x,
208
+ y: dagreNode.y,
209
+ width: dagreNode.width,
210
+ height: dagreNode.height,
211
+ collapsed: true,
212
+ childCount: collapseInfo?.collapsedChildCounts.get(label) ?? 0,
213
+ });
214
+ }
215
+
216
+ // Compute parallel edge offsets
217
+ const edgeYOffsets: number[] = new Array(parsed.edges.length).fill(0);
218
+ const edgeParallelCounts: number[] = new Array(parsed.edges.length).fill(1);
219
+ const parallelGroups = new Map<string, number[]>();
220
+
221
+ for (let i = 0; i < parsed.edges.length; i++) {
222
+ const edge = parsed.edges[i];
223
+ // Normalize key so A→B and B→A are in the same parallel group
224
+ const [a, b] =
225
+ edge.source < edge.target
226
+ ? [edge.source, edge.target]
227
+ : [edge.target, edge.source];
228
+ const key = `${a}\x00${b}`;
229
+ if (!parallelGroups.has(key)) parallelGroups.set(key, []);
230
+ parallelGroups.get(key)!.push(i);
231
+ }
232
+
233
+ for (const group of parallelGroups.values()) {
234
+ const capped = group.slice(0, MAX_PARALLEL_EDGES);
235
+ for (const idx of group.slice(MAX_PARALLEL_EDGES)) {
236
+ edgeParallelCounts[idx] = 0;
237
+ }
238
+ if (capped.length < 2) continue;
239
+ const effectiveSpacing = Math.min(
240
+ PARALLEL_SPACING,
241
+ (60 - PARALLEL_EDGE_MARGIN) / (capped.length - 1)
242
+ );
243
+ for (let j = 0; j < capped.length; j++) {
244
+ edgeYOffsets[capped[j]] =
245
+ (j - (capped.length - 1) / 2) * effectiveSpacing;
246
+ edgeParallelCounts[capped[j]] = capped.length;
247
+ }
248
+ }
249
+
250
+ // Extract edge points
251
+ const deferredSet = new Set(deferredEdgeIndices);
252
+ const layoutEdges: BLLayoutEdge[] = [];
253
+ for (let i = 0; i < parsed.edges.length; i++) {
254
+ const edge = parsed.edges[i];
255
+ if (edgeParallelCounts[i] === 0) continue;
256
+
257
+ let points: { x: number; y: number }[];
258
+
259
+ if (deferredSet.has(i)) {
260
+ // Deferred edge (compound parent endpoint) — compute points from node positions
261
+ const srcNode = g.node(edge.source);
262
+ const tgtNode = g.node(edge.target);
263
+ if (!srcNode || !tgtNode) continue;
264
+ const midX = (srcNode.x + tgtNode.x) / 2;
265
+ const midY = (srcNode.y + tgtNode.y) / 2;
266
+ points = [
267
+ { x: srcNode.x, y: srcNode.y },
268
+ { x: midX, y: midY },
269
+ { x: tgtNode.x, y: tgtNode.y },
270
+ ];
271
+ } else {
272
+ const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
273
+ points = dagreEdge?.points ?? [];
274
+ }
275
+
276
+ // Compute label position at midpoint
277
+ let labelX: number | undefined;
278
+ let labelY: number | undefined;
279
+ if (edge.label && points.length >= 2) {
280
+ const mid = Math.floor(points.length / 2);
281
+ labelX = points[mid].x;
282
+ labelY = points[mid].y - 10;
283
+ }
284
+
285
+ layoutEdges.push({
286
+ source: edge.source,
287
+ target: edge.target,
288
+ label: edge.label,
289
+ bidirectional: edge.bidirectional,
290
+ lineNumber: edge.lineNumber,
291
+ points,
292
+ labelX,
293
+ labelY,
294
+ yOffset: edgeYOffsets[i],
295
+ parallelCount: edgeParallelCounts[i],
296
+ metadata: edge.metadata,
297
+ });
298
+ }
299
+
300
+ // Compute total dimensions
301
+ let maxX = 0;
302
+ let maxY = 0;
303
+ for (const node of layoutNodes) {
304
+ maxX = Math.max(maxX, node.x + node.width / 2);
305
+ maxY = Math.max(maxY, node.y + node.height / 2);
306
+ }
307
+ for (const group of layoutGroups) {
308
+ maxX = Math.max(maxX, group.x + group.width / 2);
309
+ maxY = Math.max(maxY, group.y + group.height / 2);
310
+ }
311
+
312
+ return {
313
+ nodes: layoutNodes,
314
+ edges: layoutEdges,
315
+ groups: layoutGroups,
316
+ width: maxX + MARGIN,
317
+ height: maxY + MARGIN,
318
+ };
319
+ }