@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/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 {
|
|
@@ -95,18 +97,17 @@ const CONTAINER_META_LINE_HEIGHT = 16;
|
|
|
95
97
|
const STACK_V_GAP = 20;
|
|
96
98
|
|
|
97
99
|
|
|
98
|
-
// Legend
|
|
100
|
+
// Legend (kanban-style pills)
|
|
99
101
|
const LEGEND_GAP = 30;
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const EYE_ICON_GAP = 6;
|
|
102
|
+
const LEGEND_HEIGHT = 28;
|
|
103
|
+
const LEGEND_PILL_PAD = 16;
|
|
104
|
+
const LEGEND_PILL_FONT_W = 11 * 0.6;
|
|
105
|
+
const LEGEND_CAPSULE_PAD = 4;
|
|
106
|
+
const LEGEND_DOT_R = 4;
|
|
107
|
+
const LEGEND_ENTRY_FONT_W = 10 * 0.6;
|
|
108
|
+
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
109
|
+
const LEGEND_ENTRY_TRAIL = 8;
|
|
110
|
+
const LEGEND_GROUP_GAP = 12;
|
|
110
111
|
|
|
111
112
|
// ============================================================
|
|
112
113
|
// Helpers
|
|
@@ -269,45 +270,35 @@ function centerHeavyChildren(node: TreeNode): void {
|
|
|
269
270
|
// Layout
|
|
270
271
|
// ============================================================
|
|
271
272
|
|
|
272
|
-
function computeLegendGroups(tagGroups: OrgTagGroup[],
|
|
273
|
+
function computeLegendGroups(tagGroups: OrgTagGroup[], _showEyeIcons: boolean): OrgLegendGroup[] {
|
|
273
274
|
const groups: OrgLegendGroup[] = [];
|
|
274
275
|
|
|
275
276
|
for (const group of tagGroups) {
|
|
276
277
|
if (group.entries.length === 0) continue;
|
|
277
278
|
|
|
278
|
-
const
|
|
279
|
-
(e) =>
|
|
280
|
-
LEGEND_DOT_R * 2 + LEGEND_DOT_TEXT_GAP + e.value.length * CHAR_WIDTH
|
|
281
|
-
);
|
|
279
|
+
const pillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
|
|
282
280
|
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
maxW = entryWidths[idx];
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (maxW > 0) colWidths.push(maxW);
|
|
281
|
+
// Capsule: pad + pill + gap + entries + pad
|
|
282
|
+
let entriesWidth = 0;
|
|
283
|
+
for (const entry of group.entries) {
|
|
284
|
+
entriesWidth +=
|
|
285
|
+
LEGEND_DOT_R * 2 +
|
|
286
|
+
LEGEND_ENTRY_DOT_GAP +
|
|
287
|
+
entry.value.length * LEGEND_ENTRY_FONT_W +
|
|
288
|
+
LEGEND_ENTRY_TRAIL;
|
|
295
289
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const headerWidth = group.name.length * CHAR_WIDTH + eyeExtra;
|
|
299
|
-
const totalColumnsWidth =
|
|
300
|
-
colWidths.reduce((s, w) => s + w, 0) +
|
|
301
|
-
(colWidths.length - 1) * LEGEND_ENTRY_GAP;
|
|
302
|
-
const maxRowWidth = Math.max(headerWidth, totalColumnsWidth);
|
|
290
|
+
const capsuleWidth =
|
|
291
|
+
LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth;
|
|
303
292
|
|
|
304
293
|
groups.push({
|
|
305
294
|
name: group.name,
|
|
306
295
|
entries: group.entries.map((e) => ({ value: e.value, color: e.color })),
|
|
307
296
|
x: 0,
|
|
308
297
|
y: 0,
|
|
309
|
-
width:
|
|
310
|
-
height:
|
|
298
|
+
width: capsuleWidth,
|
|
299
|
+
height: LEGEND_HEIGHT,
|
|
300
|
+
minifiedWidth: pillWidth,
|
|
301
|
+
minifiedHeight: LEGEND_HEIGHT,
|
|
311
302
|
});
|
|
312
303
|
}
|
|
313
304
|
|
|
@@ -350,7 +341,28 @@ export function layoutOrg(
|
|
|
350
341
|
hiddenAttributes?: Set<string>
|
|
351
342
|
): OrgLayoutResult {
|
|
352
343
|
if (parsed.roots.length === 0) {
|
|
353
|
-
|
|
344
|
+
// Legend-only: compute and position legend groups even without nodes
|
|
345
|
+
const showEyeIcons = hiddenAttributes !== undefined;
|
|
346
|
+
const legendGroups = computeLegendGroups(parsed.tagGroups, showEyeIcons);
|
|
347
|
+
if (legendGroups.length === 0) {
|
|
348
|
+
return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Layout legend groups horizontally (all minified when no nodes)
|
|
352
|
+
let cx = MARGIN;
|
|
353
|
+
for (const g of legendGroups) {
|
|
354
|
+
g.x = cx;
|
|
355
|
+
g.y = MARGIN;
|
|
356
|
+
cx += g.minifiedWidth + LEGEND_GROUP_GAP;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
nodes: [],
|
|
360
|
+
edges: [],
|
|
361
|
+
containers: [],
|
|
362
|
+
legend: legendGroups,
|
|
363
|
+
width: cx - LEGEND_GROUP_GAP + MARGIN,
|
|
364
|
+
height: LEGEND_HEIGHT + MARGIN * 2,
|
|
365
|
+
};
|
|
354
366
|
}
|
|
355
367
|
|
|
356
368
|
// Inject default tag group values into node metadata for display.
|
|
@@ -577,18 +589,26 @@ export function layoutOrg(
|
|
|
577
589
|
{
|
|
578
590
|
type HNode = (typeof h);
|
|
579
591
|
const subtreeExtent = (node: HNode): { minX: number; maxX: number } => {
|
|
580
|
-
|
|
581
|
-
let
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
+
// Start with this node's own card/header bounds
|
|
593
|
+
let min = node.x! - node.data.width / 2;
|
|
594
|
+
let max = node.x! + node.data.width / 2;
|
|
595
|
+
|
|
596
|
+
// Include children's subtree extents
|
|
597
|
+
if (node.children) {
|
|
598
|
+
for (const child of node.children) {
|
|
599
|
+
const childExt = subtreeExtent(child);
|
|
600
|
+
if (childExt.minX < min) min = childExt.minX;
|
|
601
|
+
if (childExt.maxX > max) max = childExt.maxX;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Container boxes wrap their content with padding — mirror the
|
|
606
|
+
// actual bounding-box computation so compaction sees the true width.
|
|
607
|
+
if (node.data.orgNode.isContainer) {
|
|
608
|
+
min -= CONTAINER_PAD_X;
|
|
609
|
+
max += CONTAINER_PAD_X;
|
|
610
|
+
}
|
|
611
|
+
|
|
592
612
|
return { minX: min, maxX: max };
|
|
593
613
|
};
|
|
594
614
|
|
|
@@ -1072,12 +1092,22 @@ export function layoutOrg(
|
|
|
1072
1092
|
|
|
1073
1093
|
const legendPosition = parsed.options?.['legend-position'] ?? 'bottom';
|
|
1074
1094
|
|
|
1075
|
-
|
|
1095
|
+
// When a tag group is active, only that group is laid out (full size).
|
|
1096
|
+
// When none is active, all groups are laid out minified.
|
|
1097
|
+
const visibleGroups = activeTagGroup != null
|
|
1098
|
+
? legendGroups.filter((g) => g.name.toLowerCase() === activeTagGroup.toLowerCase())
|
|
1099
|
+
: legendGroups;
|
|
1100
|
+
const effectiveW = (g: OrgLegendGroup) =>
|
|
1101
|
+
activeTagGroup != null ? g.width : g.minifiedWidth;
|
|
1102
|
+
const effectiveH = (g: OrgLegendGroup) =>
|
|
1103
|
+
activeTagGroup != null ? g.height : g.minifiedHeight;
|
|
1104
|
+
|
|
1105
|
+
if (visibleGroups.length > 0) {
|
|
1076
1106
|
if (legendPosition === 'bottom') {
|
|
1077
1107
|
// Bottom: center legend groups horizontally below diagram content
|
|
1078
1108
|
const totalGroupsWidth =
|
|
1079
|
-
|
|
1080
|
-
(
|
|
1109
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1110
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1081
1111
|
const neededWidth = totalGroupsWidth + MARGIN * 2;
|
|
1082
1112
|
|
|
1083
1113
|
if (neededWidth > totalWidth) {
|
|
@@ -1095,33 +1125,34 @@ export function layoutOrg(
|
|
|
1095
1125
|
const startX = (finalWidth - totalGroupsWidth) / 2;
|
|
1096
1126
|
|
|
1097
1127
|
let cx = startX;
|
|
1098
|
-
|
|
1099
|
-
for (const g of legendGroups) {
|
|
1128
|
+
for (const g of visibleGroups) {
|
|
1100
1129
|
g.x = cx;
|
|
1101
1130
|
g.y = legendY;
|
|
1102
|
-
cx += g
|
|
1103
|
-
if (g.height > maxH) maxH = g.height;
|
|
1131
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1104
1132
|
}
|
|
1105
1133
|
|
|
1106
|
-
finalHeight = totalHeight + LEGEND_GAP +
|
|
1134
|
+
finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT;
|
|
1107
1135
|
} else {
|
|
1108
|
-
// Top
|
|
1109
|
-
const
|
|
1136
|
+
// Top: horizontal row at top-right
|
|
1137
|
+
const totalGroupsWidth =
|
|
1138
|
+
visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
1139
|
+
(visibleGroups.length - 1) * LEGEND_GROUP_GAP;
|
|
1110
1140
|
const legendStartX = totalWidth - MARGIN + LEGEND_GAP;
|
|
1111
|
-
|
|
1141
|
+
const legendY = MARGIN;
|
|
1112
1142
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1143
|
+
let cx = legendStartX;
|
|
1144
|
+
for (const g of visibleGroups) {
|
|
1145
|
+
g.x = cx;
|
|
1115
1146
|
g.y = legendY;
|
|
1116
|
-
|
|
1147
|
+
cx += effectiveW(g) + LEGEND_GROUP_GAP;
|
|
1117
1148
|
}
|
|
1118
1149
|
|
|
1119
|
-
const legendRight = legendStartX +
|
|
1150
|
+
const legendRight = legendStartX + totalGroupsWidth + MARGIN;
|
|
1120
1151
|
if (legendRight > finalWidth) {
|
|
1121
1152
|
finalWidth = legendRight;
|
|
1122
1153
|
}
|
|
1123
1154
|
|
|
1124
|
-
const legendBottom = legendY
|
|
1155
|
+
const legendBottom = legendY + LEGEND_HEIGHT + MARGIN;
|
|
1125
1156
|
if (legendBottom > finalHeight) {
|
|
1126
1157
|
finalHeight = legendBottom;
|
|
1127
1158
|
}
|
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
|
|
|
@@ -76,6 +79,20 @@ const CHART_TYPE_RE = /^chart\s*:\s*(.+)/i;
|
|
|
76
79
|
const TITLE_RE = /^title\s*:\s*(.+)/i;
|
|
77
80
|
const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
78
81
|
|
|
82
|
+
// ============================================================
|
|
83
|
+
// Inference
|
|
84
|
+
// ============================================================
|
|
85
|
+
|
|
86
|
+
/** Returns true if content contains tag group headings (`## ...`), suggesting an org chart. */
|
|
87
|
+
export function looksLikeOrg(content: string): boolean {
|
|
88
|
+
for (const line of content.split('\n')) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
91
|
+
if (GROUP_HEADING_RE.test(trimmed)) return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
// ============================================================
|
|
80
97
|
// Parser
|
|
81
98
|
// ============================================================
|
|
@@ -90,12 +107,26 @@ export function parseOrg(
|
|
|
90
107
|
roots: [],
|
|
91
108
|
tagGroups: [],
|
|
92
109
|
options: {},
|
|
110
|
+
diagnostics: [],
|
|
93
111
|
error: null,
|
|
94
112
|
};
|
|
95
113
|
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
const fail = (line: number, message: string): ParsedOrg => {
|
|
115
|
+
const diag = makeDgmoError(line, message);
|
|
116
|
+
result.diagnostics.push(diag);
|
|
117
|
+
result.error = formatDgmoError(diag);
|
|
98
118
|
return result;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** Push a recoverable error and continue parsing. */
|
|
122
|
+
const pushError = (line: number, message: string): void => {
|
|
123
|
+
const diag = makeDgmoError(line, message);
|
|
124
|
+
result.diagnostics.push(diag);
|
|
125
|
+
if (!result.error) result.error = formatDgmoError(diag);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (!content || !content.trim()) {
|
|
129
|
+
return fail(0, 'No content provided');
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
const lines = content.split('\n');
|
|
@@ -138,8 +169,11 @@ export function parseOrg(
|
|
|
138
169
|
if (chartMatch) {
|
|
139
170
|
const chartType = chartMatch[1].trim().toLowerCase();
|
|
140
171
|
if (chartType !== 'org') {
|
|
141
|
-
|
|
142
|
-
|
|
172
|
+
const allTypes = ['org', 'class', 'flowchart', 'sequence', 'er', 'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline', 'arc', 'slope'];
|
|
173
|
+
let msg = `Expected chart type "org", got "${chartType}"`;
|
|
174
|
+
const hint = suggest(chartType, allTypes);
|
|
175
|
+
if (hint) msg += `. ${hint}`;
|
|
176
|
+
return fail(lineNumber, msg);
|
|
143
177
|
}
|
|
144
178
|
continue;
|
|
145
179
|
}
|
|
@@ -172,8 +206,8 @@ export function parseOrg(
|
|
|
172
206
|
const groupMatch = trimmed.match(GROUP_HEADING_RE);
|
|
173
207
|
if (groupMatch) {
|
|
174
208
|
if (contentStarted) {
|
|
175
|
-
|
|
176
|
-
|
|
209
|
+
pushError(lineNumber, 'Tag groups (##) must appear before org content');
|
|
210
|
+
continue;
|
|
177
211
|
}
|
|
178
212
|
const groupName = groupMatch[1].trim();
|
|
179
213
|
const alias = groupMatch[2] || undefined;
|
|
@@ -201,8 +235,8 @@ export function parseOrg(
|
|
|
201
235
|
: trimmed;
|
|
202
236
|
const { label, color } = extractColor(entryText, palette);
|
|
203
237
|
if (!color) {
|
|
204
|
-
|
|
205
|
-
|
|
238
|
+
pushError(lineNumber, `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`);
|
|
239
|
+
continue;
|
|
206
240
|
}
|
|
207
241
|
if (isDefault) {
|
|
208
242
|
currentTagGroup.defaultValue = label;
|
|
@@ -259,10 +293,10 @@ export function parseOrg(
|
|
|
259
293
|
// Find the parent node: top of stack (the most recent node)
|
|
260
294
|
const parent = findMetadataParent(indent, indentStack);
|
|
261
295
|
if (!parent) {
|
|
262
|
-
|
|
263
|
-
|
|
296
|
+
pushError(lineNumber, 'Metadata has no parent node');
|
|
297
|
+
} else {
|
|
298
|
+
parent.metadata[key] = value;
|
|
264
299
|
}
|
|
265
|
-
parent.metadata[key] = value;
|
|
266
300
|
} else if (metadataMatch && indentStack.length === 0) {
|
|
267
301
|
// Metadata with no parent — could be a node label that happens to contain ':'
|
|
268
302
|
// Treat it as a node if it's at indent 0 and no nodes exist yet
|
|
@@ -272,8 +306,7 @@ export function parseOrg(
|
|
|
272
306
|
const node = parseNodeLabel(trimmed, indent, lineNumber, palette, ++nodeCounter, aliasMap);
|
|
273
307
|
attachNode(node, indent, indentStack, result);
|
|
274
308
|
} else {
|
|
275
|
-
|
|
276
|
-
return result;
|
|
309
|
+
pushError(lineNumber, 'Metadata has no parent node');
|
|
277
310
|
}
|
|
278
311
|
} else {
|
|
279
312
|
// It's a node label — possibly with single-line pipe-delimited metadata
|
|
@@ -282,8 +315,10 @@ export function parseOrg(
|
|
|
282
315
|
}
|
|
283
316
|
}
|
|
284
317
|
|
|
285
|
-
if (result.roots.length === 0 && !result.error) {
|
|
286
|
-
|
|
318
|
+
if (result.roots.length === 0 && result.tagGroups.length === 0 && !result.error) {
|
|
319
|
+
const diag = makeDgmoError(1, 'No nodes found in org chart');
|
|
320
|
+
result.diagnostics.push(diag);
|
|
321
|
+
result.error = formatDgmoError(diag);
|
|
287
322
|
}
|
|
288
323
|
|
|
289
324
|
return result;
|