@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
|
@@ -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';
|
|
@@ -139,6 +147,9 @@ export { renderOrg, renderOrgForExport } from './org/renderer';
|
|
|
139
147
|
export { collapseOrgTree } from './org/collapse';
|
|
140
148
|
export type { CollapsedOrgResult } from './org/collapse';
|
|
141
149
|
|
|
150
|
+
export { resolveOrgImports } from './org/resolver';
|
|
151
|
+
export type { ReadFileFn, ResolveImportsResult } from './org/resolver';
|
|
152
|
+
|
|
142
153
|
export { layoutGraph } from './graph/layout';
|
|
143
154
|
export type {
|
|
144
155
|
LayoutResult,
|
package/src/org/layout.ts
CHANGED
|
@@ -61,6 +61,8 @@ export interface OrgLegendGroup {
|
|
|
61
61
|
y: number;
|
|
62
62
|
width: number;
|
|
63
63
|
height: number;
|
|
64
|
+
minifiedWidth: number;
|
|
65
|
+
minifiedHeight: number;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export interface OrgLayoutResult {
|
|
@@ -301,6 +303,8 @@ function computeLegendGroups(tagGroups: OrgTagGroup[], showEyeIcons: boolean): O
|
|
|
301
303
|
(colWidths.length - 1) * LEGEND_ENTRY_GAP;
|
|
302
304
|
const maxRowWidth = Math.max(headerWidth, totalColumnsWidth);
|
|
303
305
|
|
|
306
|
+
const minifiedWidth = group.name.length * CHAR_WIDTH + LEGEND_PAD * 2;
|
|
307
|
+
|
|
304
308
|
groups.push({
|
|
305
309
|
name: group.name,
|
|
306
310
|
entries: group.entries.map((e) => ({ value: e.value, color: e.color })),
|
|
@@ -308,6 +312,8 @@ function computeLegendGroups(tagGroups: OrgTagGroup[], showEyeIcons: boolean): O
|
|
|
308
312
|
y: 0,
|
|
309
313
|
width: maxRowWidth + LEGEND_PAD * 2,
|
|
310
314
|
height: LEGEND_HEADER_H + numRows * LEGEND_ENTRY_H + LEGEND_PAD,
|
|
315
|
+
minifiedWidth,
|
|
316
|
+
minifiedHeight: LEGEND_HEADER_H + LEGEND_PAD,
|
|
311
317
|
});
|
|
312
318
|
}
|
|
313
319
|
|
|
@@ -577,18 +583,26 @@ export function layoutOrg(
|
|
|
577
583
|
{
|
|
578
584
|
type HNode = (typeof h);
|
|
579
585
|
const subtreeExtent = (node: HNode): { minX: number; maxX: number } => {
|
|
580
|
-
|
|
581
|
-
let
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
586
|
+
// Start with this node's own card/header bounds
|
|
587
|
+
let min = node.x! - node.data.width / 2;
|
|
588
|
+
let max = node.x! + node.data.width / 2;
|
|
589
|
+
|
|
590
|
+
// Include children's subtree extents
|
|
591
|
+
if (node.children) {
|
|
592
|
+
for (const child of node.children) {
|
|
593
|
+
const childExt = subtreeExtent(child);
|
|
594
|
+
if (childExt.minX < min) min = childExt.minX;
|
|
595
|
+
if (childExt.maxX > max) max = childExt.maxX;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Container boxes wrap their content with padding — mirror the
|
|
600
|
+
// actual bounding-box computation so compaction sees the true width.
|
|
601
|
+
if (node.data.orgNode.isContainer) {
|
|
602
|
+
min -= CONTAINER_PAD_X;
|
|
603
|
+
max += CONTAINER_PAD_X;
|
|
604
|
+
}
|
|
605
|
+
|
|
592
606
|
return { minX: min, maxX: max };
|
|
593
607
|
};
|
|
594
608
|
|
|
@@ -1072,12 +1086,22 @@ export function layoutOrg(
|
|
|
1072
1086
|
|
|
1073
1087
|
const legendPosition = parsed.options?.['legend-position'] ?? 'bottom';
|
|
1074
1088
|
|
|
1075
|
-
|
|
1089
|
+
// When a tag group is active, only that group is laid out (full size).
|
|
1090
|
+
// When none is active, all groups are laid out minified.
|
|
1091
|
+
const visibleGroups = activeTagGroup != null
|
|
1092
|
+
? legendGroups.filter((g) => g.name.toLowerCase() === activeTagGroup.toLowerCase())
|
|
1093
|
+
: legendGroups;
|
|
1094
|
+
const effectiveW = (g: OrgLegendGroup) =>
|
|
1095
|
+
activeTagGroup != null ? g.width : g.minifiedWidth;
|
|
1096
|
+
const effectiveH = (g: OrgLegendGroup) =>
|
|
1097
|
+
activeTagGroup != null ? g.height : g.minifiedHeight;
|
|
1098
|
+
|
|
1099
|
+
if (visibleGroups.length > 0) {
|
|
1076
1100
|
if (legendPosition === 'bottom') {
|
|
1077
1101
|
// Bottom: center legend groups horizontally below diagram content
|
|
1078
1102
|
const totalGroupsWidth =
|
|
1079
|
-
|
|
1080
|
-
(
|
|
1103
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1104
|
+
(visibleGroups.length - 1) * H_GAP;
|
|
1081
1105
|
const neededWidth = totalGroupsWidth + MARGIN * 2;
|
|
1082
1106
|
|
|
1083
1107
|
if (neededWidth > totalWidth) {
|
|
@@ -1096,24 +1120,25 @@ export function layoutOrg(
|
|
|
1096
1120
|
|
|
1097
1121
|
let cx = startX;
|
|
1098
1122
|
let maxH = 0;
|
|
1099
|
-
for (const g of
|
|
1123
|
+
for (const g of visibleGroups) {
|
|
1100
1124
|
g.x = cx;
|
|
1101
1125
|
g.y = legendY;
|
|
1102
|
-
cx += g
|
|
1103
|
-
|
|
1126
|
+
cx += effectiveW(g) + H_GAP;
|
|
1127
|
+
const h = effectiveH(g);
|
|
1128
|
+
if (h > maxH) maxH = h;
|
|
1104
1129
|
}
|
|
1105
1130
|
|
|
1106
1131
|
finalHeight = totalHeight + LEGEND_GAP + maxH;
|
|
1107
1132
|
} else {
|
|
1108
1133
|
// Top (default): stack legend groups vertically at top-right
|
|
1109
|
-
const maxLegendWidth = Math.max(...
|
|
1134
|
+
const maxLegendWidth = Math.max(...visibleGroups.map((g) => effectiveW(g)));
|
|
1110
1135
|
const legendStartX = totalWidth - MARGIN + LEGEND_GAP;
|
|
1111
1136
|
let legendY = MARGIN;
|
|
1112
1137
|
|
|
1113
|
-
for (const g of
|
|
1138
|
+
for (const g of visibleGroups) {
|
|
1114
1139
|
g.x = legendStartX;
|
|
1115
1140
|
g.y = legendY;
|
|
1116
|
-
legendY += g
|
|
1141
|
+
legendY += effectiveH(g) + LEGEND_V_GAP;
|
|
1117
1142
|
}
|
|
1118
1143
|
|
|
1119
1144
|
const legendRight = legendStartX + maxLegendWidth + MARGIN;
|
package/src/org/parser.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { resolveColor } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
|
+
import type { DgmoError } from '../diagnostics';
|
|
4
|
+
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
3
5
|
|
|
4
6
|
// ============================================================
|
|
5
7
|
// Types
|
|
@@ -37,6 +39,7 @@ export interface ParsedOrg {
|
|
|
37
39
|
roots: OrgNode[];
|
|
38
40
|
tagGroups: OrgTagGroup[];
|
|
39
41
|
options: Record<string, string>;
|
|
42
|
+
diagnostics: DgmoError[];
|
|
40
43
|
error: string | null;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -90,12 +93,26 @@ export function parseOrg(
|
|
|
90
93
|
roots: [],
|
|
91
94
|
tagGroups: [],
|
|
92
95
|
options: {},
|
|
96
|
+
diagnostics: [],
|
|
93
97
|
error: null,
|
|
94
98
|
};
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
const fail = (line: number, message: string): ParsedOrg => {
|
|
101
|
+
const diag = makeDgmoError(line, message);
|
|
102
|
+
result.diagnostics.push(diag);
|
|
103
|
+
result.error = formatDgmoError(diag);
|
|
98
104
|
return result;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Push a recoverable error and continue parsing. */
|
|
108
|
+
const pushError = (line: number, message: string): void => {
|
|
109
|
+
const diag = makeDgmoError(line, message);
|
|
110
|
+
result.diagnostics.push(diag);
|
|
111
|
+
if (!result.error) result.error = formatDgmoError(diag);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (!content || !content.trim()) {
|
|
115
|
+
return fail(0, 'No content provided');
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
const lines = content.split('\n');
|
|
@@ -138,8 +155,11 @@ export function parseOrg(
|
|
|
138
155
|
if (chartMatch) {
|
|
139
156
|
const chartType = chartMatch[1].trim().toLowerCase();
|
|
140
157
|
if (chartType !== 'org') {
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
const allTypes = ['org', 'class', 'flowchart', 'sequence', 'er', 'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline', 'arc', 'slope'];
|
|
159
|
+
let msg = `Expected chart type "org", got "${chartType}"`;
|
|
160
|
+
const hint = suggest(chartType, allTypes);
|
|
161
|
+
if (hint) msg += `. ${hint}`;
|
|
162
|
+
return fail(lineNumber, msg);
|
|
143
163
|
}
|
|
144
164
|
continue;
|
|
145
165
|
}
|
|
@@ -172,8 +192,8 @@ export function parseOrg(
|
|
|
172
192
|
const groupMatch = trimmed.match(GROUP_HEADING_RE);
|
|
173
193
|
if (groupMatch) {
|
|
174
194
|
if (contentStarted) {
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
pushError(lineNumber, 'Tag groups (##) must appear before org content');
|
|
196
|
+
continue;
|
|
177
197
|
}
|
|
178
198
|
const groupName = groupMatch[1].trim();
|
|
179
199
|
const alias = groupMatch[2] || undefined;
|
|
@@ -201,8 +221,8 @@ export function parseOrg(
|
|
|
201
221
|
: trimmed;
|
|
202
222
|
const { label, color } = extractColor(entryText, palette);
|
|
203
223
|
if (!color) {
|
|
204
|
-
|
|
205
|
-
|
|
224
|
+
pushError(lineNumber, `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`);
|
|
225
|
+
continue;
|
|
206
226
|
}
|
|
207
227
|
if (isDefault) {
|
|
208
228
|
currentTagGroup.defaultValue = label;
|
|
@@ -259,10 +279,10 @@ export function parseOrg(
|
|
|
259
279
|
// Find the parent node: top of stack (the most recent node)
|
|
260
280
|
const parent = findMetadataParent(indent, indentStack);
|
|
261
281
|
if (!parent) {
|
|
262
|
-
|
|
263
|
-
|
|
282
|
+
pushError(lineNumber, 'Metadata has no parent node');
|
|
283
|
+
} else {
|
|
284
|
+
parent.metadata[key] = value;
|
|
264
285
|
}
|
|
265
|
-
parent.metadata[key] = value;
|
|
266
286
|
} else if (metadataMatch && indentStack.length === 0) {
|
|
267
287
|
// Metadata with no parent — could be a node label that happens to contain ':'
|
|
268
288
|
// Treat it as a node if it's at indent 0 and no nodes exist yet
|
|
@@ -272,8 +292,7 @@ export function parseOrg(
|
|
|
272
292
|
const node = parseNodeLabel(trimmed, indent, lineNumber, palette, ++nodeCounter, aliasMap);
|
|
273
293
|
attachNode(node, indent, indentStack, result);
|
|
274
294
|
} else {
|
|
275
|
-
|
|
276
|
-
return result;
|
|
295
|
+
pushError(lineNumber, 'Metadata has no parent node');
|
|
277
296
|
}
|
|
278
297
|
} else {
|
|
279
298
|
// It's a node label — possibly with single-line pipe-delimited metadata
|
|
@@ -283,7 +302,9 @@ export function parseOrg(
|
|
|
283
302
|
}
|
|
284
303
|
|
|
285
304
|
if (result.roots.length === 0 && !result.error) {
|
|
286
|
-
|
|
305
|
+
const diag = makeDgmoError(1, 'No nodes found in org chart');
|
|
306
|
+
result.diagnostics.push(diag);
|
|
307
|
+
result.error = formatDgmoError(diag);
|
|
287
308
|
}
|
|
288
309
|
|
|
289
310
|
return result;
|
package/src/org/renderer.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { layoutOrg } from './layout';
|
|
|
15
15
|
// ============================================================
|
|
16
16
|
|
|
17
17
|
const DIAGRAM_PADDING = 20;
|
|
18
|
+
const MAX_SCALE = 3;
|
|
18
19
|
const TITLE_HEIGHT = 30;
|
|
19
20
|
const TITLE_FONT_SIZE = 18;
|
|
20
21
|
const LABEL_FONT_SIZE = 13;
|
|
@@ -144,7 +145,7 @@ export function renderOrg(
|
|
|
144
145
|
const diagramH = layout.height + titleOffset;
|
|
145
146
|
const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
|
|
146
147
|
const scaleY = (height - DIAGRAM_PADDING * 2) / diagramH;
|
|
147
|
-
const scale = Math.min(scaleX, scaleY);
|
|
148
|
+
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
148
149
|
|
|
149
150
|
// Center the diagram
|
|
150
151
|
const scaledW = diagramW * scale;
|
|
@@ -209,6 +210,7 @@ export function renderOrg(
|
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
// Render container backgrounds (bottom layer)
|
|
213
|
+
const colorOff = parsed.options?.color === 'off';
|
|
212
214
|
for (const c of layout.containers) {
|
|
213
215
|
const cG = contentG
|
|
214
216
|
.append('g')
|
|
@@ -234,8 +236,8 @@ export function renderOrg(
|
|
|
234
236
|
});
|
|
235
237
|
}
|
|
236
238
|
|
|
237
|
-
const fill = containerFill(palette, isDark, c.color);
|
|
238
|
-
const stroke = containerStroke(palette, c.color);
|
|
239
|
+
const fill = containerFill(palette, isDark, colorOff ? undefined : c.color);
|
|
240
|
+
const stroke = containerStroke(palette, colorOff ? undefined : c.color);
|
|
239
241
|
|
|
240
242
|
// Background rect
|
|
241
243
|
cG.append('rect')
|
|
@@ -301,7 +303,7 @@ export function renderOrg(
|
|
|
301
303
|
.attr('y', c.height - COLLAPSE_BAR_HEIGHT)
|
|
302
304
|
.attr('width', c.width - COLLAPSE_BAR_INSET * 2)
|
|
303
305
|
.attr('height', COLLAPSE_BAR_HEIGHT)
|
|
304
|
-
.attr('fill', containerStroke(palette, c.color))
|
|
306
|
+
.attr('fill', containerStroke(palette, colorOff ? undefined : c.color))
|
|
305
307
|
.attr('clip-path', `url(#${clipId})`)
|
|
306
308
|
.attr('class', 'org-collapse-bar');
|
|
307
309
|
}
|
|
@@ -360,8 +362,8 @@ export function renderOrg(
|
|
|
360
362
|
}
|
|
361
363
|
|
|
362
364
|
// Card background
|
|
363
|
-
const fill = nodeFill(palette, isDark, node.color);
|
|
364
|
-
const stroke = nodeStroke(palette, node.color);
|
|
365
|
+
const fill = nodeFill(palette, isDark, colorOff ? undefined : node.color);
|
|
366
|
+
const stroke = nodeStroke(palette, colorOff ? undefined : node.color);
|
|
365
367
|
|
|
366
368
|
const rect = nodeG
|
|
367
369
|
.append('rect')
|
|
@@ -448,22 +450,30 @@ export function renderOrg(
|
|
|
448
450
|
.attr('y', node.height - COLLAPSE_BAR_HEIGHT)
|
|
449
451
|
.attr('width', node.width - COLLAPSE_BAR_INSET * 2)
|
|
450
452
|
.attr('height', COLLAPSE_BAR_HEIGHT)
|
|
451
|
-
.attr('fill', nodeStroke(palette, node.color))
|
|
453
|
+
.attr('fill', nodeStroke(palette, colorOff ? undefined : node.color))
|
|
452
454
|
.attr('clip-path', `url(#${clipId})`)
|
|
453
455
|
.attr('class', 'org-collapse-bar');
|
|
454
456
|
}
|
|
455
457
|
|
|
456
458
|
}
|
|
457
459
|
|
|
458
|
-
// Render legend — skip entirely in export mode
|
|
460
|
+
// Render legend — skip entirely in export mode.
|
|
461
|
+
// No active group: all groups rendered minified (compact chips).
|
|
462
|
+
// Active group selected: only that group rendered (full size), others hidden.
|
|
459
463
|
if (!exportDims) for (const group of layout.legend) {
|
|
460
464
|
const isActive =
|
|
461
465
|
activeTagGroup != null &&
|
|
462
466
|
group.name.toLowerCase() === activeTagGroup.toLowerCase();
|
|
463
467
|
|
|
464
|
-
// When a group is active, skip
|
|
468
|
+
// When a group is active, skip all other groups entirely
|
|
465
469
|
if (activeTagGroup != null && !isActive) continue;
|
|
466
470
|
|
|
471
|
+
// No active group → minified; active group → full
|
|
472
|
+
const isMinified = activeTagGroup == null;
|
|
473
|
+
|
|
474
|
+
const renderW = isMinified ? group.minifiedWidth : group.width;
|
|
475
|
+
const renderH = isMinified ? group.minifiedHeight : group.height;
|
|
476
|
+
|
|
467
477
|
const gEl = contentG
|
|
468
478
|
.append('g')
|
|
469
479
|
.attr('transform', `translate(${group.x}, ${group.y})`)
|
|
@@ -477,8 +487,8 @@ export function renderOrg(
|
|
|
477
487
|
.append('rect')
|
|
478
488
|
.attr('x', 0)
|
|
479
489
|
.attr('y', 0)
|
|
480
|
-
.attr('width',
|
|
481
|
-
.attr('height',
|
|
490
|
+
.attr('width', renderW)
|
|
491
|
+
.attr('height', renderH)
|
|
482
492
|
.attr('rx', LEGEND_RADIUS)
|
|
483
493
|
.attr('fill', legendFill);
|
|
484
494
|
|
|
@@ -499,11 +509,14 @@ export function renderOrg(
|
|
|
499
509
|
.append('text')
|
|
500
510
|
.attr('x', LEGEND_PAD)
|
|
501
511
|
.attr('y', LEGEND_HEADER_H / 2 + LEGEND_FONT_SIZE / 2 - 2)
|
|
502
|
-
.attr('fill', palette.text)
|
|
512
|
+
.attr('fill', isMinified ? palette.textMuted : palette.text)
|
|
503
513
|
.attr('font-size', LEGEND_FONT_SIZE)
|
|
504
514
|
.attr('font-weight', 'bold')
|
|
505
515
|
.text(group.name);
|
|
506
516
|
|
|
517
|
+
// Minified groups only show the header — skip entries and eye icon
|
|
518
|
+
if (isMinified) continue;
|
|
519
|
+
|
|
507
520
|
// Eye icon for visibility toggle (interactive only, not export)
|
|
508
521
|
if (hiddenAttributes !== undefined && !exportDims) {
|
|
509
522
|
const groupKey = group.name.toLowerCase();
|