@diagrammo/dgmo 0.2.19 → 0.2.21
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 +33 -33
- package/dist/cli.cjs +150 -144
- package/dist/index.cjs +9475 -8087
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -1
- package/dist/index.d.ts +124 -1
- package/dist/index.js +9345 -7965
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chart.ts +40 -9
- package/src/class/parser.ts +37 -6
- package/src/class/renderer.ts +11 -8
- package/src/class/types.ts +4 -0
- package/src/cli.ts +38 -3
- package/src/d3.ts +159 -48
- package/src/dgmo-mermaid.ts +7 -1
- package/src/dgmo-router.ts +74 -4
- package/src/diagnostics.ts +77 -0
- package/src/echarts.ts +23 -14
- package/src/er/layout.ts +49 -7
- package/src/er/parser.ts +31 -4
- package/src/er/renderer.ts +2 -1
- package/src/er/types.ts +3 -0
- package/src/graph/flowchart-parser.ts +34 -4
- package/src/graph/flowchart-renderer.ts +35 -32
- package/src/graph/types.ts +4 -0
- package/src/index.ts +22 -0
- package/src/kanban/mutations.ts +183 -0
- package/src/kanban/parser.ts +389 -0
- package/src/kanban/renderer.ts +564 -0
- package/src/kanban/types.ts +45 -0
- package/src/org/layout.ts +97 -66
- package/src/org/parser.ts +50 -15
- package/src/org/renderer.ts +91 -159
- package/src/org/resolver.ts +470 -0
- package/src/sequence/parser.ts +90 -33
- package/src/sequence/renderer.ts +13 -5
package/src/er/layout.ts
CHANGED
|
@@ -153,17 +153,59 @@ export function layoutERDiagram(parsed: ParsedERDiagram): ERLayoutResult {
|
|
|
153
153
|
};
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
// Compute total dimensions
|
|
157
|
-
let
|
|
158
|
-
let
|
|
156
|
+
// Compute total dimensions from nodes, edge points, and labels
|
|
157
|
+
let minX = Infinity;
|
|
158
|
+
let minY = Infinity;
|
|
159
|
+
let maxX = 0;
|
|
160
|
+
let maxY = 0;
|
|
159
161
|
for (const node of layoutNodes) {
|
|
162
|
+
const left = node.x - node.width / 2;
|
|
160
163
|
const right = node.x + node.width / 2;
|
|
164
|
+
const top = node.y - node.height / 2;
|
|
161
165
|
const bottom = node.y + node.height / 2;
|
|
162
|
-
if (
|
|
163
|
-
if (
|
|
166
|
+
if (left < minX) minX = left;
|
|
167
|
+
if (right > maxX) maxX = right;
|
|
168
|
+
if (top < minY) minY = top;
|
|
169
|
+
if (bottom > maxY) maxY = bottom;
|
|
164
170
|
}
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
for (const edge of layoutEdges) {
|
|
172
|
+
for (const pt of edge.points) {
|
|
173
|
+
if (pt.x < minX) minX = pt.x;
|
|
174
|
+
if (pt.x > maxX) maxX = pt.x;
|
|
175
|
+
if (pt.y < minY) minY = pt.y;
|
|
176
|
+
if (pt.y > maxY) maxY = pt.y;
|
|
177
|
+
}
|
|
178
|
+
// Edge labels extend ~50px from midpoint
|
|
179
|
+
if (edge.label && edge.points.length > 0) {
|
|
180
|
+
const midPt = edge.points[Math.floor(edge.points.length / 2)];
|
|
181
|
+
const labelHalfW = (edge.label.length * 7 + 8) / 2;
|
|
182
|
+
if (midPt.x + labelHalfW > maxX) maxX = midPt.x + labelHalfW;
|
|
183
|
+
if (midPt.x - labelHalfW < minX) minX = midPt.x - labelHalfW;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Padding for cardinality markers (~25px) + edge labels
|
|
188
|
+
const EDGE_MARGIN = 60;
|
|
189
|
+
const HALF_MARGIN = EDGE_MARGIN / 2;
|
|
190
|
+
|
|
191
|
+
// Shift all nodes and edges so content starts at HALF_MARGIN (breathing room on all sides)
|
|
192
|
+
const shiftX = -minX + HALF_MARGIN;
|
|
193
|
+
const shiftY = -minY + HALF_MARGIN;
|
|
194
|
+
for (const node of layoutNodes) {
|
|
195
|
+
node.x += shiftX;
|
|
196
|
+
node.y += shiftY;
|
|
197
|
+
}
|
|
198
|
+
for (const edge of layoutEdges) {
|
|
199
|
+
for (const pt of edge.points) {
|
|
200
|
+
pt.x += shiftX;
|
|
201
|
+
pt.y += shiftY;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
maxX += shiftX;
|
|
205
|
+
maxY += shiftY;
|
|
206
|
+
|
|
207
|
+
const totalWidth = maxX + HALF_MARGIN;
|
|
208
|
+
const totalHeight = maxY + HALF_MARGIN;
|
|
167
209
|
|
|
168
210
|
return {
|
|
169
211
|
nodes: layoutNodes,
|
package/src/er/parser.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveColor } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
|
+
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
3
4
|
import type {
|
|
4
5
|
ParsedERDiagram,
|
|
5
6
|
ERTable,
|
|
@@ -155,6 +156,14 @@ export function parseERDiagram(
|
|
|
155
156
|
options: {},
|
|
156
157
|
tables: [],
|
|
157
158
|
relationships: [],
|
|
159
|
+
diagnostics: [],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const fail = (line: number, message: string): ParsedERDiagram => {
|
|
163
|
+
const diag = makeDgmoError(line, message);
|
|
164
|
+
result.diagnostics.push(diag);
|
|
165
|
+
result.error = formatDgmoError(diag);
|
|
166
|
+
return result;
|
|
158
167
|
};
|
|
159
168
|
|
|
160
169
|
const tableMap = new Map<string, ERTable>();
|
|
@@ -200,8 +209,11 @@ export function parseERDiagram(
|
|
|
200
209
|
|
|
201
210
|
if (key === 'chart') {
|
|
202
211
|
if (value.toLowerCase() !== 'er') {
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
const allTypes = ['er', 'class', 'flowchart', 'sequence', 'org', 'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline', 'arc', 'slope'];
|
|
213
|
+
let msg = `Expected chart type "er", got "${value}"`;
|
|
214
|
+
const hint = suggest(value.toLowerCase(), allTypes);
|
|
215
|
+
if (hint) msg += `. ${hint}`;
|
|
216
|
+
return fail(lineNumber, msg);
|
|
205
217
|
}
|
|
206
218
|
continue;
|
|
207
219
|
}
|
|
@@ -278,8 +290,23 @@ export function parseERDiagram(
|
|
|
278
290
|
|
|
279
291
|
// Validation
|
|
280
292
|
if (result.tables.length === 0 && !result.error) {
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
const diag = makeDgmoError(1, 'No tables found. Add table declarations like "users" or "orders (blue)".');
|
|
294
|
+
result.diagnostics.push(diag);
|
|
295
|
+
result.error = formatDgmoError(diag);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Warn about isolated tables (not in any relationship)
|
|
299
|
+
if (result.tables.length >= 2 && result.relationships.length >= 1 && !result.error) {
|
|
300
|
+
const connectedIds = new Set<string>();
|
|
301
|
+
for (const rel of result.relationships) {
|
|
302
|
+
connectedIds.add(rel.source);
|
|
303
|
+
connectedIds.add(rel.target);
|
|
304
|
+
}
|
|
305
|
+
for (const table of result.tables) {
|
|
306
|
+
if (!connectedIds.has(table.id)) {
|
|
307
|
+
result.diagnostics.push(makeDgmoError(table.lineNumber, `Table "${table.name}" is not connected to any other table`, 'warning'));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
283
310
|
}
|
|
284
311
|
|
|
285
312
|
return result;
|
package/src/er/renderer.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { layoutERDiagram } from './layout';
|
|
|
17
17
|
// ============================================================
|
|
18
18
|
|
|
19
19
|
const DIAGRAM_PADDING = 20;
|
|
20
|
+
const MAX_SCALE = 3;
|
|
20
21
|
const TABLE_FONT_SIZE = 13;
|
|
21
22
|
const COLUMN_FONT_SIZE = 11;
|
|
22
23
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
@@ -227,7 +228,7 @@ export function renderERDiagram(
|
|
|
227
228
|
const availH = height - titleHeight;
|
|
228
229
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
229
230
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
230
|
-
const scale = Math.min(
|
|
231
|
+
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
231
232
|
|
|
232
233
|
const scaledW = diagramW * scale;
|
|
233
234
|
const scaledH = diagramH * scale;
|
package/src/er/types.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface ERRelationship {
|
|
|
29
29
|
lineNumber: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
import type { DgmoError } from '../diagnostics';
|
|
33
|
+
|
|
32
34
|
export interface ParsedERDiagram {
|
|
33
35
|
type: 'er';
|
|
34
36
|
title?: string;
|
|
@@ -36,5 +38,6 @@ export interface ParsedERDiagram {
|
|
|
36
38
|
options: Record<string, string>;
|
|
37
39
|
tables: ERTable[];
|
|
38
40
|
relationships: ERRelationship[];
|
|
41
|
+
diagnostics: DgmoError[];
|
|
39
42
|
error?: string;
|
|
40
43
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveColor } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
|
+
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
3
4
|
import type {
|
|
4
5
|
ParsedGraph,
|
|
5
6
|
GraphNode,
|
|
@@ -236,6 +237,15 @@ export function parseFlowchart(
|
|
|
236
237
|
direction: 'TB',
|
|
237
238
|
nodes: [],
|
|
238
239
|
edges: [],
|
|
240
|
+
options: {},
|
|
241
|
+
diagnostics: [],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const fail = (line: number, message: string): ParsedGraph => {
|
|
245
|
+
const diag = makeDgmoError(line, message);
|
|
246
|
+
result.diagnostics.push(diag);
|
|
247
|
+
result.error = formatDgmoError(diag);
|
|
248
|
+
return result;
|
|
239
249
|
};
|
|
240
250
|
|
|
241
251
|
const nodeMap = new Map<string, GraphNode>();
|
|
@@ -428,8 +438,11 @@ export function parseFlowchart(
|
|
|
428
438
|
|
|
429
439
|
if (key === 'chart') {
|
|
430
440
|
if (value.toLowerCase() !== 'flowchart') {
|
|
431
|
-
|
|
432
|
-
|
|
441
|
+
const allTypes = ['flowchart', 'sequence', 'class', 'er', 'org', 'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline', 'arc', 'slope'];
|
|
442
|
+
let msg = `Expected chart type "flowchart", got "${value}"`;
|
|
443
|
+
const hint = suggest(value.toLowerCase(), allTypes);
|
|
444
|
+
if (hint) msg += `. ${hint}`;
|
|
445
|
+
return fail(lineNumber, msg);
|
|
433
446
|
}
|
|
434
447
|
continue;
|
|
435
448
|
}
|
|
@@ -448,7 +461,8 @@ export function parseFlowchart(
|
|
|
448
461
|
continue;
|
|
449
462
|
}
|
|
450
463
|
|
|
451
|
-
//
|
|
464
|
+
// Store other options (e.g., color: off)
|
|
465
|
+
result.options[key] = value;
|
|
452
466
|
continue;
|
|
453
467
|
}
|
|
454
468
|
|
|
@@ -460,7 +474,23 @@ export function parseFlowchart(
|
|
|
460
474
|
|
|
461
475
|
// Validation: no nodes found
|
|
462
476
|
if (result.nodes.length === 0 && !result.error) {
|
|
463
|
-
|
|
477
|
+
const diag = makeDgmoError(1, 'No nodes found. Add flowchart content with shape syntax like [Process] or (Start).');
|
|
478
|
+
result.diagnostics.push(diag);
|
|
479
|
+
result.error = formatDgmoError(diag);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Warn about orphaned nodes (not referenced in any edge)
|
|
483
|
+
if (result.nodes.length >= 2 && result.edges.length >= 1 && !result.error) {
|
|
484
|
+
const connectedIds = new Set<string>();
|
|
485
|
+
for (const edge of result.edges) {
|
|
486
|
+
connectedIds.add(edge.source);
|
|
487
|
+
connectedIds.add(edge.target);
|
|
488
|
+
}
|
|
489
|
+
for (const node of result.nodes) {
|
|
490
|
+
if (!connectedIds.has(node.id)) {
|
|
491
|
+
result.diagnostics.push(makeDgmoError(node.lineNumber, `Node "${node.label}" is not connected to any other node`, 'warning'));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
464
494
|
}
|
|
465
495
|
|
|
466
496
|
return result;
|
|
@@ -16,6 +16,7 @@ import { layoutGraph } from './layout';
|
|
|
16
16
|
// ============================================================
|
|
17
17
|
|
|
18
18
|
const DIAGRAM_PADDING = 20;
|
|
19
|
+
const MAX_SCALE = 3;
|
|
19
20
|
const NODE_FONT_SIZE = 13;
|
|
20
21
|
const EDGE_LABEL_FONT_SIZE = 11;
|
|
21
22
|
const GROUP_LABEL_FONT_SIZE = 11;
|
|
@@ -43,7 +44,8 @@ function mix(a: string, b: string, pct: number): string {
|
|
|
43
44
|
return `#${c(ar,br)}${c(ag,bg)}${c(ab,bb)}`;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function shapeDefaultColor(shape: GraphShape, palette: PaletteColors, isEndTerminal?: boolean): string {
|
|
47
|
+
function shapeDefaultColor(shape: GraphShape, palette: PaletteColors, isEndTerminal?: boolean, colorOff?: boolean): string {
|
|
48
|
+
if (colorOff) return palette.textMuted;
|
|
47
49
|
switch (shape) {
|
|
48
50
|
case 'terminal': return isEndTerminal ? palette.colors.red : palette.colors.green;
|
|
49
51
|
case 'process': return palette.colors.blue;
|
|
@@ -54,13 +56,13 @@ function shapeDefaultColor(shape: GraphShape, palette: PaletteColors, isEndTermi
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
function nodeFill(palette: PaletteColors, isDark: boolean, shape: GraphShape, nodeColor?: string, isEndTerminal?: boolean): string {
|
|
58
|
-
const color = nodeColor ?? shapeDefaultColor(shape, palette, isEndTerminal);
|
|
59
|
+
function nodeFill(palette: PaletteColors, isDark: boolean, shape: GraphShape, nodeColor?: string, isEndTerminal?: boolean, colorOff?: boolean): string {
|
|
60
|
+
const color = nodeColor ?? shapeDefaultColor(shape, palette, isEndTerminal, colorOff);
|
|
59
61
|
return mix(color, isDark ? palette.surface : palette.bg, 25);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
function nodeStroke(palette: PaletteColors, shape: GraphShape, nodeColor?: string, isEndTerminal?: boolean): string {
|
|
63
|
-
return nodeColor ?? shapeDefaultColor(shape, palette, isEndTerminal);
|
|
64
|
+
function nodeStroke(palette: PaletteColors, shape: GraphShape, nodeColor?: string, isEndTerminal?: boolean, colorOff?: boolean): string {
|
|
65
|
+
return nodeColor ?? shapeDefaultColor(shape, palette, isEndTerminal, colorOff);
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
// ============================================================
|
|
@@ -69,7 +71,7 @@ function nodeStroke(palette: PaletteColors, shape: GraphShape, nodeColor?: strin
|
|
|
69
71
|
|
|
70
72
|
type GSelection = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
71
73
|
|
|
72
|
-
function renderTerminal(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, isEnd: boolean): void {
|
|
74
|
+
function renderTerminal(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, isEnd: boolean, colorOff?: boolean): void {
|
|
73
75
|
const w = node.width;
|
|
74
76
|
const h = node.height;
|
|
75
77
|
const rx = h / 2;
|
|
@@ -80,12 +82,12 @@ function renderTerminal(g: GSelection, node: LayoutNode, palette: PaletteColors,
|
|
|
80
82
|
.attr('height', h)
|
|
81
83
|
.attr('rx', rx)
|
|
82
84
|
.attr('ry', rx)
|
|
83
|
-
.attr('fill', nodeFill(palette, isDark, node.shape, node.color, isEnd))
|
|
84
|
-
.attr('stroke', nodeStroke(palette, node.shape, node.color, isEnd))
|
|
85
|
+
.attr('fill', nodeFill(palette, isDark, node.shape, node.color, isEnd, colorOff))
|
|
86
|
+
.attr('stroke', nodeStroke(palette, node.shape, node.color, isEnd, colorOff))
|
|
85
87
|
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
function renderProcess(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
90
|
+
function renderProcess(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, colorOff?: boolean): void {
|
|
89
91
|
const w = node.width;
|
|
90
92
|
const h = node.height;
|
|
91
93
|
g.append('rect')
|
|
@@ -95,12 +97,12 @@ function renderProcess(g: GSelection, node: LayoutNode, palette: PaletteColors,
|
|
|
95
97
|
.attr('height', h)
|
|
96
98
|
.attr('rx', 3)
|
|
97
99
|
.attr('ry', 3)
|
|
98
|
-
.attr('fill', nodeFill(palette, isDark, node.shape, node.color))
|
|
99
|
-
.attr('stroke', nodeStroke(palette, node.shape, node.color))
|
|
100
|
+
.attr('fill', nodeFill(palette, isDark, node.shape, node.color, undefined, colorOff))
|
|
101
|
+
.attr('stroke', nodeStroke(palette, node.shape, node.color, undefined, colorOff))
|
|
100
102
|
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
function renderDecision(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
105
|
+
function renderDecision(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, colorOff?: boolean): void {
|
|
104
106
|
const w = node.width / 2;
|
|
105
107
|
const h = node.height / 2;
|
|
106
108
|
const points = [
|
|
@@ -111,12 +113,12 @@ function renderDecision(g: GSelection, node: LayoutNode, palette: PaletteColors,
|
|
|
111
113
|
].join(' ');
|
|
112
114
|
g.append('polygon')
|
|
113
115
|
.attr('points', points)
|
|
114
|
-
.attr('fill', nodeFill(palette, isDark, node.shape, node.color))
|
|
115
|
-
.attr('stroke', nodeStroke(palette, node.shape, node.color))
|
|
116
|
+
.attr('fill', nodeFill(palette, isDark, node.shape, node.color, undefined, colorOff))
|
|
117
|
+
.attr('stroke', nodeStroke(palette, node.shape, node.color, undefined, colorOff))
|
|
116
118
|
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
function renderIO(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
121
|
+
function renderIO(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, colorOff?: boolean): void {
|
|
120
122
|
const w = node.width / 2;
|
|
121
123
|
const h = node.height / 2;
|
|
122
124
|
const sk = IO_SKEW;
|
|
@@ -128,15 +130,15 @@ function renderIO(g: GSelection, node: LayoutNode, palette: PaletteColors, isDar
|
|
|
128
130
|
].join(' ');
|
|
129
131
|
g.append('polygon')
|
|
130
132
|
.attr('points', points)
|
|
131
|
-
.attr('fill', nodeFill(palette, isDark, node.shape, node.color))
|
|
132
|
-
.attr('stroke', nodeStroke(palette, node.shape, node.color))
|
|
133
|
+
.attr('fill', nodeFill(palette, isDark, node.shape, node.color, undefined, colorOff))
|
|
134
|
+
.attr('stroke', nodeStroke(palette, node.shape, node.color, undefined, colorOff))
|
|
133
135
|
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
function renderSubroutine(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
138
|
+
function renderSubroutine(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, colorOff?: boolean): void {
|
|
137
139
|
const w = node.width;
|
|
138
140
|
const h = node.height;
|
|
139
|
-
const s = nodeStroke(palette, node.shape, node.color);
|
|
141
|
+
const s = nodeStroke(palette, node.shape, node.color, undefined, colorOff);
|
|
140
142
|
// Outer rectangle
|
|
141
143
|
g.append('rect')
|
|
142
144
|
.attr('x', -w / 2)
|
|
@@ -145,7 +147,7 @@ function renderSubroutine(g: GSelection, node: LayoutNode, palette: PaletteColor
|
|
|
145
147
|
.attr('height', h)
|
|
146
148
|
.attr('rx', 3)
|
|
147
149
|
.attr('ry', 3)
|
|
148
|
-
.attr('fill', nodeFill(palette, isDark, node.shape, node.color))
|
|
150
|
+
.attr('fill', nodeFill(palette, isDark, node.shape, node.color, undefined, colorOff))
|
|
149
151
|
.attr('stroke', s)
|
|
150
152
|
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
151
153
|
// Left inner border
|
|
@@ -166,7 +168,7 @@ function renderSubroutine(g: GSelection, node: LayoutNode, palette: PaletteColor
|
|
|
166
168
|
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
function renderDocument(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean): void {
|
|
171
|
+
function renderDocument(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, colorOff?: boolean): void {
|
|
170
172
|
const w = node.width;
|
|
171
173
|
const h = node.height;
|
|
172
174
|
const waveH = DOC_WAVE_HEIGHT;
|
|
@@ -186,30 +188,30 @@ function renderDocument(g: GSelection, node: LayoutNode, palette: PaletteColors,
|
|
|
186
188
|
|
|
187
189
|
g.append('path')
|
|
188
190
|
.attr('d', d)
|
|
189
|
-
.attr('fill', nodeFill(palette, isDark, node.shape, node.color))
|
|
190
|
-
.attr('stroke', nodeStroke(palette, node.shape, node.color))
|
|
191
|
+
.attr('fill', nodeFill(palette, isDark, node.shape, node.color, undefined, colorOff))
|
|
192
|
+
.attr('stroke', nodeStroke(palette, node.shape, node.color, undefined, colorOff))
|
|
191
193
|
.attr('stroke-width', NODE_STROKE_WIDTH);
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
function renderNodeShape(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, endTerminalIds: Set<string
|
|
196
|
+
function renderNodeShape(g: GSelection, node: LayoutNode, palette: PaletteColors, isDark: boolean, endTerminalIds: Set<string>, colorOff?: boolean): void {
|
|
195
197
|
switch (node.shape) {
|
|
196
198
|
case 'terminal':
|
|
197
|
-
renderTerminal(g, node, palette, isDark, endTerminalIds.has(node.id));
|
|
199
|
+
renderTerminal(g, node, palette, isDark, endTerminalIds.has(node.id), colorOff);
|
|
198
200
|
break;
|
|
199
201
|
case 'process':
|
|
200
|
-
renderProcess(g, node, palette, isDark);
|
|
202
|
+
renderProcess(g, node, palette, isDark, colorOff);
|
|
201
203
|
break;
|
|
202
204
|
case 'decision':
|
|
203
|
-
renderDecision(g, node, palette, isDark);
|
|
205
|
+
renderDecision(g, node, palette, isDark, colorOff);
|
|
204
206
|
break;
|
|
205
207
|
case 'io':
|
|
206
|
-
renderIO(g, node, palette, isDark);
|
|
208
|
+
renderIO(g, node, palette, isDark, colorOff);
|
|
207
209
|
break;
|
|
208
210
|
case 'subroutine':
|
|
209
|
-
renderSubroutine(g, node, palette, isDark);
|
|
211
|
+
renderSubroutine(g, node, palette, isDark, colorOff);
|
|
210
212
|
break;
|
|
211
213
|
case 'document':
|
|
212
|
-
renderDocument(g, node, palette, isDark);
|
|
214
|
+
renderDocument(g, node, palette, isDark, colorOff);
|
|
213
215
|
break;
|
|
214
216
|
}
|
|
215
217
|
}
|
|
@@ -251,7 +253,7 @@ export function renderFlowchart(
|
|
|
251
253
|
const availH = height - titleHeight;
|
|
252
254
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
253
255
|
const scaleY = (availH - DIAGRAM_PADDING * 2) / diagramH;
|
|
254
|
-
const scale = Math.min(
|
|
256
|
+
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
255
257
|
|
|
256
258
|
// Center the diagram in the area below the title
|
|
257
259
|
const scaledW = diagramW * scale;
|
|
@@ -443,6 +445,7 @@ export function renderFlowchart(
|
|
|
443
445
|
}
|
|
444
446
|
|
|
445
447
|
// Render nodes (top layer)
|
|
448
|
+
const colorOff = graph.options?.color === 'off';
|
|
446
449
|
for (const node of layout.nodes) {
|
|
447
450
|
const nodeG = contentG
|
|
448
451
|
.append('g')
|
|
@@ -458,7 +461,7 @@ export function renderFlowchart(
|
|
|
458
461
|
}
|
|
459
462
|
|
|
460
463
|
// Shape
|
|
461
|
-
renderNodeShape(nodeG as GSelection, node, palette, isDark, endTerminalIds);
|
|
464
|
+
renderNodeShape(nodeG as GSelection, node, palette, isDark, endTerminalIds, colorOff);
|
|
462
465
|
|
|
463
466
|
// Label
|
|
464
467
|
nodeG
|
package/src/graph/types.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface GraphGroup {
|
|
|
33
33
|
lineNumber: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
import type { DgmoError } from '../diagnostics';
|
|
37
|
+
|
|
36
38
|
export interface ParsedGraph {
|
|
37
39
|
type: 'flowchart';
|
|
38
40
|
title?: string;
|
|
@@ -41,5 +43,7 @@ export interface ParsedGraph {
|
|
|
41
43
|
nodes: GraphNode[];
|
|
42
44
|
edges: GraphEdge[];
|
|
43
45
|
groups?: GraphGroup[];
|
|
46
|
+
options: Record<string, string>;
|
|
47
|
+
diagnostics: DgmoError[];
|
|
44
48
|
error?: string;
|
|
45
49
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Diagnostics
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export { makeDgmoError, formatDgmoError } from './diagnostics';
|
|
6
|
+
export type { DgmoError, DgmoSeverity } from './diagnostics';
|
|
7
|
+
|
|
1
8
|
// ============================================================
|
|
2
9
|
// Unified API
|
|
3
10
|
// ============================================================
|
|
@@ -11,6 +18,7 @@ export { render } from './render';
|
|
|
11
18
|
export {
|
|
12
19
|
parseDgmoChartType,
|
|
13
20
|
getDgmoFramework,
|
|
21
|
+
parseDgmo,
|
|
14
22
|
DGMO_CHART_TYPE_MAP,
|
|
15
23
|
} from './dgmo-router';
|
|
16
24
|
export type { DgmoFramework } from './dgmo-router';
|
|
@@ -136,9 +144,23 @@ export type {
|
|
|
136
144
|
|
|
137
145
|
export { renderOrg, renderOrgForExport } from './org/renderer';
|
|
138
146
|
|
|
147
|
+
export { parseKanban } from './kanban/parser';
|
|
148
|
+
export type {
|
|
149
|
+
ParsedKanban,
|
|
150
|
+
KanbanColumn,
|
|
151
|
+
KanbanCard,
|
|
152
|
+
KanbanTagGroup,
|
|
153
|
+
KanbanTagEntry,
|
|
154
|
+
} from './kanban/types';
|
|
155
|
+
export { computeCardMove, computeCardArchive, isArchiveColumn } from './kanban/mutations';
|
|
156
|
+
export { renderKanban, renderKanbanForExport } from './kanban/renderer';
|
|
157
|
+
|
|
139
158
|
export { collapseOrgTree } from './org/collapse';
|
|
140
159
|
export type { CollapsedOrgResult } from './org/collapse';
|
|
141
160
|
|
|
161
|
+
export { resolveOrgImports } from './org/resolver';
|
|
162
|
+
export type { ReadFileFn, ResolveImportsResult } from './org/resolver';
|
|
163
|
+
|
|
142
164
|
export { layoutGraph } from './graph/layout';
|
|
143
165
|
export type {
|
|
144
166
|
LayoutResult,
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ParsedKanban, KanbanCard, KanbanColumn } from './types';
|
|
2
|
+
|
|
3
|
+
const ARCHIVE_COLUMN_NAME = 'archive';
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// computeCardMove — pure function for source text mutation
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compute new file content after moving a card to a different position.
|
|
11
|
+
*
|
|
12
|
+
* @param content - original file content string
|
|
13
|
+
* @param parsed - parsed kanban board
|
|
14
|
+
* @param cardId - id of the card to move
|
|
15
|
+
* @param targetColumnId - id of the destination column
|
|
16
|
+
* @param targetIndex - position within target column (0 = first card)
|
|
17
|
+
* @returns new content string, or null if move is invalid
|
|
18
|
+
*/
|
|
19
|
+
export function computeCardMove(
|
|
20
|
+
content: string,
|
|
21
|
+
parsed: ParsedKanban,
|
|
22
|
+
cardId: string,
|
|
23
|
+
targetColumnId: string,
|
|
24
|
+
targetIndex: number
|
|
25
|
+
): string | null {
|
|
26
|
+
// Find source card and column
|
|
27
|
+
let sourceCard: KanbanCard | null = null;
|
|
28
|
+
let sourceColumn: KanbanColumn | null = null;
|
|
29
|
+
|
|
30
|
+
for (const col of parsed.columns) {
|
|
31
|
+
for (const card of col.cards) {
|
|
32
|
+
if (card.id === cardId) {
|
|
33
|
+
sourceCard = card;
|
|
34
|
+
sourceColumn = col;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (sourceCard) break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!sourceCard || !sourceColumn) return null;
|
|
42
|
+
|
|
43
|
+
const targetColumn = parsed.columns.find((c) => c.id === targetColumnId);
|
|
44
|
+
if (!targetColumn) return null;
|
|
45
|
+
|
|
46
|
+
const lines = content.split('\n');
|
|
47
|
+
|
|
48
|
+
// Extract the card's lines (0-based indices)
|
|
49
|
+
const startIdx = sourceCard.lineNumber - 1;
|
|
50
|
+
const endIdx = sourceCard.endLineNumber - 1;
|
|
51
|
+
const cardLines = lines.slice(startIdx, endIdx + 1);
|
|
52
|
+
|
|
53
|
+
// Remove the card lines from content
|
|
54
|
+
const withoutCard = [
|
|
55
|
+
...lines.slice(0, startIdx),
|
|
56
|
+
...lines.slice(endIdx + 1),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Compute insertion point (0-based index in withoutCard)
|
|
60
|
+
let insertIdx: number;
|
|
61
|
+
|
|
62
|
+
// Adjust target column and card line numbers after removal
|
|
63
|
+
// Lines after the removed range shift up by the number of removed lines
|
|
64
|
+
const removedCount = endIdx - startIdx + 1;
|
|
65
|
+
const adjustLine = (ln: number): number => {
|
|
66
|
+
// ln is 1-based
|
|
67
|
+
if (ln > endIdx + 1) return ln - removedCount;
|
|
68
|
+
return ln;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (targetIndex === 0) {
|
|
72
|
+
// Insert right after column header line
|
|
73
|
+
const adjColLine = adjustLine(targetColumn.lineNumber);
|
|
74
|
+
insertIdx = adjColLine; // 0-based: insert after the column header line
|
|
75
|
+
} else {
|
|
76
|
+
// Insert after the preceding card's last line
|
|
77
|
+
// Get the cards in the target column, excluding the moved card
|
|
78
|
+
const targetCards = targetColumn.cards.filter((c) => c.id !== cardId);
|
|
79
|
+
const clampedIdx = Math.min(targetIndex, targetCards.length);
|
|
80
|
+
const precedingCard = targetCards[clampedIdx - 1];
|
|
81
|
+
if (!precedingCard) {
|
|
82
|
+
// Fallback: after column header
|
|
83
|
+
const adjColLine = adjustLine(targetColumn.lineNumber);
|
|
84
|
+
insertIdx = adjColLine;
|
|
85
|
+
} else {
|
|
86
|
+
const adjEndLine = adjustLine(precedingCard.endLineNumber);
|
|
87
|
+
insertIdx = adjEndLine; // 0-based: insert after this line
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Splice the card lines into the new position
|
|
92
|
+
const result = [
|
|
93
|
+
...withoutCard.slice(0, insertIdx),
|
|
94
|
+
...cardLines,
|
|
95
|
+
...withoutCard.slice(insertIdx),
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
return result.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================
|
|
102
|
+
// computeCardArchive — move card to an Archive section
|
|
103
|
+
// ============================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Move a card to the Archive section at the end of the file.
|
|
107
|
+
* Creates `== Archive ==` if it doesn't exist.
|
|
108
|
+
*
|
|
109
|
+
* @returns new content string, or null if the card is not found
|
|
110
|
+
*/
|
|
111
|
+
export function computeCardArchive(
|
|
112
|
+
content: string,
|
|
113
|
+
parsed: ParsedKanban,
|
|
114
|
+
cardId: string
|
|
115
|
+
): string | null {
|
|
116
|
+
let sourceCard: KanbanCard | null = null;
|
|
117
|
+
for (const col of parsed.columns) {
|
|
118
|
+
for (const card of col.cards) {
|
|
119
|
+
if (card.id === cardId) {
|
|
120
|
+
sourceCard = card;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (sourceCard) break;
|
|
125
|
+
}
|
|
126
|
+
if (!sourceCard) return null;
|
|
127
|
+
|
|
128
|
+
const lines = content.split('\n');
|
|
129
|
+
|
|
130
|
+
// Extract card lines
|
|
131
|
+
const startIdx = sourceCard.lineNumber - 1;
|
|
132
|
+
const endIdx = sourceCard.endLineNumber - 1;
|
|
133
|
+
const cardLines = lines.slice(startIdx, endIdx + 1);
|
|
134
|
+
|
|
135
|
+
// Remove card from its current position
|
|
136
|
+
const withoutCard = [
|
|
137
|
+
...lines.slice(0, startIdx),
|
|
138
|
+
...lines.slice(endIdx + 1),
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// Check if an Archive column already exists
|
|
142
|
+
const archiveCol = parsed.columns.find(
|
|
143
|
+
(c) => c.name.toLowerCase() === ARCHIVE_COLUMN_NAME
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (archiveCol) {
|
|
147
|
+
// Append to existing archive column
|
|
148
|
+
// Find the last line of the archive column (after removal adjustment)
|
|
149
|
+
const removedCount = endIdx - startIdx + 1;
|
|
150
|
+
let archiveEndLine = archiveCol.lineNumber;
|
|
151
|
+
if (archiveCol.cards.length > 0) {
|
|
152
|
+
const lastCard = archiveCol.cards[archiveCol.cards.length - 1];
|
|
153
|
+
archiveEndLine = lastCard.endLineNumber;
|
|
154
|
+
}
|
|
155
|
+
// Adjust for removed lines
|
|
156
|
+
if (archiveEndLine > endIdx + 1) {
|
|
157
|
+
archiveEndLine -= removedCount;
|
|
158
|
+
}
|
|
159
|
+
// Insert after the archive end (0-based in withoutCard)
|
|
160
|
+
const insertIdx = archiveEndLine; // archiveEndLine is 1-based, so this is after that line
|
|
161
|
+
return [
|
|
162
|
+
...withoutCard.slice(0, insertIdx),
|
|
163
|
+
...cardLines,
|
|
164
|
+
...withoutCard.slice(insertIdx),
|
|
165
|
+
].join('\n');
|
|
166
|
+
} else {
|
|
167
|
+
// Create archive section at end of file
|
|
168
|
+
// Ensure trailing newline before the new section
|
|
169
|
+
const trimmedEnd = withoutCard.length > 0 && withoutCard[withoutCard.length - 1].trim() === ''
|
|
170
|
+
? withoutCard
|
|
171
|
+
: [...withoutCard, ''];
|
|
172
|
+
return [
|
|
173
|
+
...trimmedEnd,
|
|
174
|
+
'== Archive ==',
|
|
175
|
+
...cardLines,
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Check if a column name is the archive column (case-insensitive). */
|
|
181
|
+
export function isArchiveColumn(name: string): boolean {
|
|
182
|
+
return name.toLowerCase() === ARCHIVE_COLUMN_NAME;
|
|
183
|
+
}
|