@diagrammo/dgmo 0.2.18 → 0.2.20
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 +148 -144
- package/dist/index.cjs +8773 -8072
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +8727 -8028
- 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 +115 -47
- package/src/dgmo-mermaid.ts +7 -1
- package/src/dgmo-router.ts +67 -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 +11 -0
- package/src/org/layout.ts +46 -21
- package/src/org/parser.ts +35 -14
- package/src/org/renderer.ts +25 -12
- package/src/org/resolver.ts +470 -0
- package/src/sequence/parser.ts +90 -33
- package/src/sequence/renderer.ts +6 -0
package/src/dgmo-router.ts
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
// .dgmo Unified Format — Chart Type Router
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
-
import { looksLikeSequence } from './sequence/parser';
|
|
6
|
-
import { looksLikeFlowchart } from './graph/flowchart-parser';
|
|
7
|
-
import { looksLikeClassDiagram } from './class/parser';
|
|
8
|
-
import { looksLikeERDiagram } from './er/parser';
|
|
5
|
+
import { looksLikeSequence, parseSequenceDgmo } from './sequence/parser';
|
|
6
|
+
import { looksLikeFlowchart, parseFlowchart } from './graph/flowchart-parser';
|
|
7
|
+
import { looksLikeClassDiagram, parseClassDiagram } from './class/parser';
|
|
8
|
+
import { looksLikeERDiagram, parseERDiagram } from './er/parser';
|
|
9
|
+
import { parseChart } from './chart';
|
|
10
|
+
import { parseEChart } from './echarts';
|
|
11
|
+
import { parseD3 } from './d3';
|
|
12
|
+
import { parseOrg } from './org/parser';
|
|
13
|
+
import type { DgmoError } from './diagnostics';
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Framework identifiers used by the .dgmo router.
|
|
@@ -85,3 +90,61 @@ export function parseDgmoChartType(content: string): string | null {
|
|
|
85
90
|
|
|
86
91
|
return null;
|
|
87
92
|
}
|
|
93
|
+
|
|
94
|
+
// Standard chart types parsed by parseChart (then rendered via ECharts)
|
|
95
|
+
const STANDARD_CHART_TYPES = new Set([
|
|
96
|
+
'bar', 'line', 'multi-line', 'area', 'pie', 'doughnut',
|
|
97
|
+
'radar', 'polar-area', 'bar-stacked',
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// ECharts-native types parsed by parseEChart
|
|
101
|
+
const ECHART_TYPES = new Set([
|
|
102
|
+
'scatter', 'sankey', 'chord', 'function', 'heatmap', 'funnel',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse DGMO content and return diagnostics without rendering.
|
|
107
|
+
* Useful for the CLI and editor to surface all errors before attempting render.
|
|
108
|
+
*/
|
|
109
|
+
export function parseDgmo(content: string): { diagnostics: DgmoError[] } {
|
|
110
|
+
const chartType = parseDgmoChartType(content);
|
|
111
|
+
|
|
112
|
+
if (!chartType) {
|
|
113
|
+
// No chart type detected — try D3 parser as fallback (it handles missing chart: line)
|
|
114
|
+
const parsed = parseD3(content);
|
|
115
|
+
return { diagnostics: parsed.diagnostics };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (chartType === 'sequence') {
|
|
119
|
+
const parsed = parseSequenceDgmo(content);
|
|
120
|
+
return { diagnostics: parsed.diagnostics };
|
|
121
|
+
}
|
|
122
|
+
if (chartType === 'flowchart') {
|
|
123
|
+
const parsed = parseFlowchart(content);
|
|
124
|
+
return { diagnostics: parsed.diagnostics };
|
|
125
|
+
}
|
|
126
|
+
if (chartType === 'class') {
|
|
127
|
+
const parsed = parseClassDiagram(content);
|
|
128
|
+
return { diagnostics: parsed.diagnostics };
|
|
129
|
+
}
|
|
130
|
+
if (chartType === 'er') {
|
|
131
|
+
const parsed = parseERDiagram(content);
|
|
132
|
+
return { diagnostics: parsed.diagnostics };
|
|
133
|
+
}
|
|
134
|
+
if (chartType === 'org') {
|
|
135
|
+
const parsed = parseOrg(content);
|
|
136
|
+
return { diagnostics: parsed.diagnostics };
|
|
137
|
+
}
|
|
138
|
+
if (STANDARD_CHART_TYPES.has(chartType)) {
|
|
139
|
+
const parsed = parseChart(content);
|
|
140
|
+
return { diagnostics: parsed.diagnostics };
|
|
141
|
+
}
|
|
142
|
+
if (ECHART_TYPES.has(chartType)) {
|
|
143
|
+
const parsed = parseEChart(content);
|
|
144
|
+
return { diagnostics: parsed.diagnostics };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// D3 types (slope, wordcloud, arc, timeline, venn, quadrant)
|
|
148
|
+
const parsed = parseD3(content);
|
|
149
|
+
return { diagnostics: parsed.diagnostics };
|
|
150
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Structured Diagnostic Types
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export type DgmoSeverity = 'error' | 'warning';
|
|
6
|
+
|
|
7
|
+
export interface DgmoError {
|
|
8
|
+
line: number; // 1-based (0 = no line info)
|
|
9
|
+
column?: number; // optional 1-based column
|
|
10
|
+
message: string; // without "Line N:" prefix
|
|
11
|
+
severity: DgmoSeverity;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function makeDgmoError(
|
|
15
|
+
line: number,
|
|
16
|
+
message: string,
|
|
17
|
+
severity: DgmoSeverity = 'error'
|
|
18
|
+
): DgmoError {
|
|
19
|
+
return { line, message, severity };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatDgmoError(err: DgmoError): string {
|
|
23
|
+
return err.line > 0 ? `Line ${err.line}: ${err.message}` : err.message;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// "Did you mean?" Suggestions
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Simple Levenshtein distance between two strings.
|
|
32
|
+
*/
|
|
33
|
+
function levenshtein(a: string, b: string): number {
|
|
34
|
+
const m = a.length;
|
|
35
|
+
const n = b.length;
|
|
36
|
+
const dp: number[] = Array(n + 1)
|
|
37
|
+
.fill(0)
|
|
38
|
+
.map((_, i) => i);
|
|
39
|
+
|
|
40
|
+
for (let i = 1; i <= m; i++) {
|
|
41
|
+
let prev = dp[0];
|
|
42
|
+
dp[0] = i;
|
|
43
|
+
for (let j = 1; j <= n; j++) {
|
|
44
|
+
const tmp = dp[j];
|
|
45
|
+
dp[j] =
|
|
46
|
+
a[i - 1] === b[j - 1]
|
|
47
|
+
? prev
|
|
48
|
+
: 1 + Math.min(prev, dp[j], dp[j - 1]);
|
|
49
|
+
prev = tmp;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return dp[n];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns a "did you mean 'X'?" suggestion if the input is close to one of the candidates.
|
|
57
|
+
* Returns null if no good match is found.
|
|
58
|
+
* Threshold: distance ≤ max(2, floor(input.length / 3))
|
|
59
|
+
*/
|
|
60
|
+
export function suggest(input: string, candidates: readonly string[]): string | null {
|
|
61
|
+
if (!input || candidates.length === 0) return null;
|
|
62
|
+
const lower = input.toLowerCase();
|
|
63
|
+
const threshold = Math.max(2, Math.floor(lower.length / 3));
|
|
64
|
+
|
|
65
|
+
let best: string | null = null;
|
|
66
|
+
let bestDist = Infinity;
|
|
67
|
+
|
|
68
|
+
for (const c of candidates) {
|
|
69
|
+
const dist = levenshtein(lower, c.toLowerCase());
|
|
70
|
+
if (dist < bestDist && dist <= threshold && dist > 0) {
|
|
71
|
+
bestDist = dist;
|
|
72
|
+
best = c;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return best ? `Did you mean '${best}'?` : null;
|
|
77
|
+
}
|
package/src/echarts.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface ParsedHeatmapRow {
|
|
|
52
52
|
lineNumber: number;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
import type { DgmoError } from './diagnostics';
|
|
56
|
+
|
|
55
57
|
export interface ParsedEChart {
|
|
56
58
|
type: EChartsChartType;
|
|
57
59
|
title?: string;
|
|
@@ -72,6 +74,7 @@ export interface ParsedEChart {
|
|
|
72
74
|
sizelabel?: string;
|
|
73
75
|
showLabels?: boolean;
|
|
74
76
|
categoryColors?: Record<string, string>;
|
|
77
|
+
diagnostics: DgmoError[];
|
|
75
78
|
error?: string;
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -84,6 +87,7 @@ import type { PaletteColors } from './palettes';
|
|
|
84
87
|
import { getSeriesColors, getSegmentColors } from './palettes';
|
|
85
88
|
import { parseChart } from './chart';
|
|
86
89
|
import type { ParsedChart } from './chart';
|
|
90
|
+
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
87
91
|
|
|
88
92
|
// ============================================================
|
|
89
93
|
// Parser
|
|
@@ -111,6 +115,7 @@ export function parseEChart(
|
|
|
111
115
|
const result: ParsedEChart = {
|
|
112
116
|
type: 'scatter',
|
|
113
117
|
data: [],
|
|
118
|
+
diagnostics: [],
|
|
114
119
|
};
|
|
115
120
|
|
|
116
121
|
// Track current category for grouped scatter charts
|
|
@@ -168,7 +173,13 @@ export function parseEChart(
|
|
|
168
173
|
) {
|
|
169
174
|
result.type = chartType;
|
|
170
175
|
} else {
|
|
171
|
-
|
|
176
|
+
const validTypes = ['scatter', 'sankey', 'chord', 'function', 'heatmap', 'funnel'];
|
|
177
|
+
let msg = `Unsupported chart type: ${value}. Supported types: ${validTypes.join(', ')}.`;
|
|
178
|
+
const hint = suggest(chartType, validTypes);
|
|
179
|
+
if (hint) msg += ` ${hint}`;
|
|
180
|
+
const diag = makeDgmoError(lineNumber, msg);
|
|
181
|
+
result.diagnostics.push(diag);
|
|
182
|
+
result.error = formatDgmoError(diag);
|
|
172
183
|
return result;
|
|
173
184
|
}
|
|
174
185
|
continue;
|
|
@@ -341,42 +352,40 @@ export function parseEChart(
|
|
|
341
352
|
}
|
|
342
353
|
}
|
|
343
354
|
|
|
355
|
+
const warn = (line: number, message: string): void => {
|
|
356
|
+
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
357
|
+
};
|
|
358
|
+
|
|
344
359
|
if (!result.error) {
|
|
345
360
|
if (result.type === 'sankey') {
|
|
346
361
|
if (!result.links || result.links.length === 0) {
|
|
347
|
-
|
|
348
|
-
'No links found. Add links in format: Source -> Target: 123';
|
|
362
|
+
warn(1, 'No links found. Add links in format: Source -> Target: 123');
|
|
349
363
|
}
|
|
350
364
|
} else if (result.type === 'chord') {
|
|
351
365
|
if (!result.links || result.links.length === 0) {
|
|
352
|
-
|
|
353
|
-
'No links found. Add links in format: Source -> Target: 123';
|
|
366
|
+
warn(1, 'No links found. Add links in format: Source -> Target: 123');
|
|
354
367
|
}
|
|
355
368
|
} else if (result.type === 'function') {
|
|
356
369
|
if (!result.functions || result.functions.length === 0) {
|
|
357
|
-
|
|
358
|
-
'No functions found. Add functions in format: Name: expression';
|
|
370
|
+
warn(1, 'No functions found. Add functions in format: Name: expression');
|
|
359
371
|
}
|
|
360
372
|
if (!result.xRange) {
|
|
361
373
|
result.xRange = { min: -10, max: 10 }; // Default range
|
|
362
374
|
}
|
|
363
375
|
} else if (result.type === 'scatter') {
|
|
364
376
|
if (!result.scatterPoints || result.scatterPoints.length === 0) {
|
|
365
|
-
|
|
366
|
-
'No scatter points found. Add points in format: Name: x, y or Name: x, y, size';
|
|
377
|
+
warn(1, 'No scatter points found. Add points in format: Name: x, y or Name: x, y, size');
|
|
367
378
|
}
|
|
368
379
|
} else if (result.type === 'heatmap') {
|
|
369
380
|
if (!result.heatmapRows || result.heatmapRows.length === 0) {
|
|
370
|
-
|
|
371
|
-
'No heatmap data found. Add data in format: RowLabel: val1, val2, val3';
|
|
381
|
+
warn(1, 'No heatmap data found. Add data in format: RowLabel: val1, val2, val3');
|
|
372
382
|
}
|
|
373
383
|
if (!result.columns || result.columns.length === 0) {
|
|
374
|
-
|
|
375
|
-
'No columns defined. Add columns in format: columns: Col1, Col2, Col3';
|
|
384
|
+
warn(1, 'No columns defined. Add columns in format: columns: Col1, Col2, Col3');
|
|
376
385
|
}
|
|
377
386
|
} else if (result.type === 'funnel') {
|
|
378
387
|
if (result.data.length === 0) {
|
|
379
|
-
|
|
388
|
+
warn(1, 'No data found. Add data in format: Label: value');
|
|
380
389
|
}
|
|
381
390
|
}
|
|
382
391
|
}
|
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;
|