@diagrammo/dgmo 0.33.0 → 0.35.0
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/.claude/commands/dgmo.md +17 -32
- package/.github/copilot-instructions.md +1 -1
- package/README.md +7 -1
- package/SKILL.md +1 -1
- package/dist/advanced.cjs +791 -614
- package/dist/advanced.d.cts +8 -51
- package/dist/advanced.d.ts +8 -51
- package/dist/advanced.js +791 -615
- package/dist/auto.cjs +793 -292
- package/dist/auto.js +95 -95
- package/dist/auto.mjs +793 -292
- package/dist/cli.cjs +145 -328
- package/dist/index.cjs +792 -291
- package/dist/index.js +792 -291
- package/docs/ai-integration.md +46 -26
- package/docs/language-reference.md +2 -2
- package/package.json +3 -3
- package/src/advanced.ts +0 -12
- package/src/boxes-and-lines/label-placement.ts +339 -0
- package/src/boxes-and-lines/layout-search.ts +16 -2
- package/src/boxes-and-lines/layout.ts +32 -7
- package/src/boxes-and-lines/renderer.ts +67 -139
- package/src/c4/parser.ts +15 -6
- package/src/chart.ts +1 -1
- package/src/class/parser.ts +6 -2
- package/src/cli-banner.ts +2 -3
- package/src/cli.ts +539 -1223
- package/src/cycle/parser.ts +4 -1
- package/src/dgmo-router.ts +16 -2
- package/src/diagnostics.ts +0 -0
- package/src/echarts.ts +5 -1
- package/src/er/parser.ts +5 -1
- package/src/gantt/parser.ts +13 -4
- package/src/graph/flowchart-parser.ts +5 -1
- package/src/graph/state-parser.ts +5 -1
- package/src/infra/parser.ts +10 -6
- package/src/journey-map/parser.ts +5 -2
- package/src/kanban/parser.ts +6 -2
- package/src/map/context-labels.ts +29 -19
- package/src/map/layout.ts +403 -108
- package/src/map/legend-band.ts +28 -0
- package/src/map/parser.ts +13 -4
- package/src/map/renderer.ts +74 -23
- package/src/map/resolver.ts +2 -2
- package/src/org/parser.ts +8 -2
- package/src/pyramid/parser.ts +4 -1
- package/src/raci/parser.ts +4 -1
- package/src/ring/parser.ts +4 -1
- package/src/sequence/parser.ts +6 -3
- package/src/sitemap/parser.ts +12 -3
- package/src/tech-radar/parser.ts +6 -3
- package/src/utils/parsing.ts +2 -2
- package/src/utils/tag-groups.ts +3 -3
- package/src/visualizations/parse.ts +9 -3
- package/src/migrate/data-comma.ts +0 -117
- package/src/migrate/embedded.ts +0 -238
- package/src/migrate/index.ts +0 -295
- package/src/migrate/line-classifier.ts +0 -128
- package/src/migrate/line-transformer.ts +0 -218
|
@@ -78,7 +78,6 @@ const GROUP_LABEL_ZONE = 32;
|
|
|
78
78
|
const RAMP_FLOOR = 15;
|
|
79
79
|
const VALUE_FONT_SIZE = 11;
|
|
80
80
|
|
|
81
|
-
type D3G = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
82
81
|
type D3Svg = d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
|
|
83
82
|
|
|
84
83
|
// ── Edge path generators ───────────────────────────────────
|
|
@@ -352,78 +351,6 @@ function ensureArrowMarkers(
|
|
|
352
351
|
}
|
|
353
352
|
}
|
|
354
353
|
|
|
355
|
-
// ── Edge label placement ───────────────────────────────────
|
|
356
|
-
|
|
357
|
-
/** Point at the half-way arc length along an edge polyline — the geometric
|
|
358
|
-
* centre of the connector, so a label sits in the gap BETWEEN the two nodes
|
|
359
|
-
* (ELK's own label anchor drifts toward the target and ends up clipped under
|
|
360
|
-
* it). Falls back gracefully for degenerate point lists. */
|
|
361
|
-
function edgePolylineMidpoint(
|
|
362
|
-
points: ReadonlyArray<{ readonly x: number; readonly y: number }>
|
|
363
|
-
): { x: number; y: number } {
|
|
364
|
-
if (points.length === 0) return { x: 0, y: 0 };
|
|
365
|
-
if (points.length === 1) return { x: points[0]!.x, y: points[0]!.y };
|
|
366
|
-
let total = 0;
|
|
367
|
-
const segLen: number[] = [];
|
|
368
|
-
for (let k = 1; k < points.length; k++) {
|
|
369
|
-
const len = Math.hypot(
|
|
370
|
-
points[k]!.x - points[k - 1]!.x,
|
|
371
|
-
points[k]!.y - points[k - 1]!.y
|
|
372
|
-
);
|
|
373
|
-
segLen.push(len);
|
|
374
|
-
total += len;
|
|
375
|
-
}
|
|
376
|
-
let half = total / 2;
|
|
377
|
-
for (let k = 1; k < points.length; k++) {
|
|
378
|
-
const len = segLen[k - 1]!;
|
|
379
|
-
if (half <= len || k === points.length - 1) {
|
|
380
|
-
const t = len === 0 ? 0 : Math.min(1, half / len);
|
|
381
|
-
return {
|
|
382
|
-
x: points[k - 1]!.x + (points[k]!.x - points[k - 1]!.x) * t,
|
|
383
|
-
y: points[k - 1]!.y + (points[k]!.y - points[k - 1]!.y) * t,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
half -= len;
|
|
387
|
-
}
|
|
388
|
-
const last = points[points.length - 1]!;
|
|
389
|
-
return { x: last.x, y: last.y };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ── Edge label overlap resolution ──────────────────────────
|
|
393
|
-
|
|
394
|
-
function resolveEdgeLabelOverlaps(
|
|
395
|
-
labels: { x: number; y: number; width: number; height: number }[]
|
|
396
|
-
): void {
|
|
397
|
-
const MAX_PASSES = 8;
|
|
398
|
-
const PAD = 4;
|
|
399
|
-
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
400
|
-
let moved = false;
|
|
401
|
-
for (let i = 0; i < labels.length; i++) {
|
|
402
|
-
for (let j = i + 1; j < labels.length; j++) {
|
|
403
|
-
// In-bounds by loop guard.
|
|
404
|
-
const a = labels[i]!;
|
|
405
|
-
const b = labels[j]!;
|
|
406
|
-
const dx = Math.abs(a.x - b.x);
|
|
407
|
-
const dy = Math.abs(a.y - b.y);
|
|
408
|
-
const overlapX = (a.width + b.width) / 2 + PAD - dx;
|
|
409
|
-
const overlapY = (a.height + b.height) / 2 + PAD - dy;
|
|
410
|
-
if (overlapX > 0 && overlapY > 0) {
|
|
411
|
-
const shift = overlapY / 2 + 1;
|
|
412
|
-
if (a.y < b.y) {
|
|
413
|
-
a.y -= shift;
|
|
414
|
-
b.y += shift;
|
|
415
|
-
} else {
|
|
416
|
-
a.y += shift;
|
|
417
|
-
b.y -= shift;
|
|
418
|
-
}
|
|
419
|
-
moved = true;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (!moved) break;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
354
|
// ── Main render function ───────────────────────────────────
|
|
428
355
|
|
|
429
356
|
interface BLRenderOptions {
|
|
@@ -847,18 +774,8 @@ export function renderBoxesAndLines(
|
|
|
847
774
|
}
|
|
848
775
|
|
|
849
776
|
// ── Render edges ───────────────────────────────────────
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
x: number;
|
|
853
|
-
y: number;
|
|
854
|
-
width: number;
|
|
855
|
-
height: number;
|
|
856
|
-
idx: number;
|
|
857
|
-
}[] = [];
|
|
858
|
-
|
|
859
|
-
// Store edge group elements for label pass
|
|
860
|
-
const edgeGroups = new Map<number, D3G>();
|
|
861
|
-
|
|
777
|
+
// Edge labels are placed in layout (label-placement.ts) and drawn AFTER the
|
|
778
|
+
// node loop (see the bl-edge-labels layer) so boxes never clip them.
|
|
862
779
|
for (let i = 0; i < layout.edges.length; i++) {
|
|
863
780
|
// In-bounds by loop guard.
|
|
864
781
|
const le = layout.edges[i]!;
|
|
@@ -885,7 +802,6 @@ export function renderBoxesAndLines(
|
|
|
885
802
|
.append('g')
|
|
886
803
|
.attr('class', 'bl-edge-group')
|
|
887
804
|
.attr('data-line-number', String(le.lineNumber));
|
|
888
|
-
edgeGroups.set(i, edgeG as unknown as D3G);
|
|
889
805
|
|
|
890
806
|
const markerId = `bl-arrow-${color.replace('#', '')}`;
|
|
891
807
|
const cx = nodeLayout.x;
|
|
@@ -950,7 +866,6 @@ export function renderBoxesAndLines(
|
|
|
950
866
|
.append('g')
|
|
951
867
|
.attr('class', 'bl-edge-group')
|
|
952
868
|
.attr('data-line-number', String(le.lineNumber));
|
|
953
|
-
edgeGroups.set(i, edgeG as unknown as D3G);
|
|
954
869
|
|
|
955
870
|
const markerId = `bl-arrow-${color.replace('#', '')}`;
|
|
956
871
|
const gen = le.straight
|
|
@@ -971,58 +886,6 @@ export function renderBoxesAndLines(
|
|
|
971
886
|
const revId = `bl-arrow-rev-${color.replace('#', '')}`;
|
|
972
887
|
path.attr('marker-start', `url(#${revId})`);
|
|
973
888
|
}
|
|
974
|
-
|
|
975
|
-
// Edge label — centred on the connector's polyline midpoint (the gap
|
|
976
|
-
// between the two nodes), NOT ELK's target-biased anchor. For parallel
|
|
977
|
-
// edges, nudge above/below so each line's label clears the line.
|
|
978
|
-
if (le.label && le.points.length > 0) {
|
|
979
|
-
const lw = measureText(le.label, sEdgeLabelFontSize);
|
|
980
|
-
const labelH = sEdgeLabelFontSize + 6;
|
|
981
|
-
const mid = edgePolylineMidpoint(le.points);
|
|
982
|
-
let ly = mid.y;
|
|
983
|
-
if (le.parallelCount > 1 && le.yOffset !== 0) {
|
|
984
|
-
ly += (le.yOffset < 0 ? -labelH : labelH) * 0.5;
|
|
985
|
-
}
|
|
986
|
-
labelPositions.push({
|
|
987
|
-
x: mid.x,
|
|
988
|
-
y: ly,
|
|
989
|
-
width: lw + 8,
|
|
990
|
-
height: labelH,
|
|
991
|
-
idx: i,
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Resolve overlaps
|
|
997
|
-
resolveEdgeLabelOverlaps(labelPositions);
|
|
998
|
-
|
|
999
|
-
// Render edge labels into their edge groups
|
|
1000
|
-
for (const lp of labelPositions) {
|
|
1001
|
-
// In-bounds: lp.idx was set from a valid index into layout.edges above.
|
|
1002
|
-
const le = layout.edges[lp.idx]!;
|
|
1003
|
-
if (!le.label) continue;
|
|
1004
|
-
|
|
1005
|
-
const edgeG = edgeGroups.get(lp.idx);
|
|
1006
|
-
const target = edgeG ?? diagramG;
|
|
1007
|
-
|
|
1008
|
-
target
|
|
1009
|
-
.append('rect')
|
|
1010
|
-
.attr('x', lp.x - lp.width / 2)
|
|
1011
|
-
.attr('y', lp.y - lp.height / 2)
|
|
1012
|
-
.attr('width', lp.width)
|
|
1013
|
-
.attr('height', lp.height)
|
|
1014
|
-
.attr('rx', 3)
|
|
1015
|
-
.attr('fill', palette.bg)
|
|
1016
|
-
.attr('opacity', 0.85);
|
|
1017
|
-
|
|
1018
|
-
target
|
|
1019
|
-
.append('text')
|
|
1020
|
-
.attr('x', lp.x)
|
|
1021
|
-
.attr('y', lp.y + sEdgeLabelFontSize / 3)
|
|
1022
|
-
.attr('text-anchor', 'middle')
|
|
1023
|
-
.attr('font-size', sEdgeLabelFontSize)
|
|
1024
|
-
.attr('fill', palette.textMuted)
|
|
1025
|
-
.text(le.label);
|
|
1026
889
|
}
|
|
1027
890
|
|
|
1028
891
|
// ── Render nodes ───────────────────────────────────────
|
|
@@ -1396,6 +1259,71 @@ export function renderBoxesAndLines(
|
|
|
1396
1259
|
}
|
|
1397
1260
|
}
|
|
1398
1261
|
|
|
1262
|
+
// ── Render edge labels ─────────────────────────────────
|
|
1263
|
+
// Drawn AFTER nodes (and groups) so the halo + text always paint on top and
|
|
1264
|
+
// are never clipped by a box. Positions/wrapping come from label-placement
|
|
1265
|
+
// (layout); this is a pure consumer. Labels stay in their own layer rather
|
|
1266
|
+
// than inside each edge group, but carry data-line-number so line-hover/click
|
|
1267
|
+
// still highlights the matching label.
|
|
1268
|
+
const labelLayer = diagramG.append('g').attr('class', 'bl-edge-labels');
|
|
1269
|
+
const labelLineHeight = sEdgeLabelFontSize * 1.3;
|
|
1270
|
+
for (const le of layout.edges) {
|
|
1271
|
+
if (!le.labelLines || le.labelLines.length === 0) continue;
|
|
1272
|
+
if (le.labelX === undefined || le.labelY === undefined) continue;
|
|
1273
|
+
const lx = le.labelX;
|
|
1274
|
+
const ly = le.labelY;
|
|
1275
|
+
|
|
1276
|
+
// Honour the same tag-value hiding as the edges themselves.
|
|
1277
|
+
if (hidden.size > 0) {
|
|
1278
|
+
let isHidden = false;
|
|
1279
|
+
for (const [groupKey, hiddenVals] of hidden) {
|
|
1280
|
+
const val = le.metadata[groupKey];
|
|
1281
|
+
if (val && hiddenVals.has(val.toLowerCase())) {
|
|
1282
|
+
isHidden = true;
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (isHidden) continue;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const lw = le.labelWidth ?? 0;
|
|
1290
|
+
const lh = le.labelHeight ?? sEdgeLabelFontSize + 6;
|
|
1291
|
+
const lines = le.labelLines;
|
|
1292
|
+
const labelG = labelLayer
|
|
1293
|
+
.append('g')
|
|
1294
|
+
.attr('class', 'bl-edge-label')
|
|
1295
|
+
.attr('data-line-number', String(le.lineNumber));
|
|
1296
|
+
|
|
1297
|
+
labelG
|
|
1298
|
+
.append('rect')
|
|
1299
|
+
.attr('x', lx - lw / 2)
|
|
1300
|
+
.attr('y', ly - lh / 2)
|
|
1301
|
+
.attr('width', lw)
|
|
1302
|
+
.attr('height', lh)
|
|
1303
|
+
.attr('rx', 3)
|
|
1304
|
+
.attr('fill', palette.bg)
|
|
1305
|
+
// No border; semi-transparent so the line stays faintly visible behind the
|
|
1306
|
+
// label while the (full-opacity) text on top stays crisp.
|
|
1307
|
+
.attr('opacity', 0.72);
|
|
1308
|
+
|
|
1309
|
+
const text = labelG
|
|
1310
|
+
.append('text')
|
|
1311
|
+
.attr('x', lx)
|
|
1312
|
+
.attr('text-anchor', 'middle')
|
|
1313
|
+
.attr('font-family', FONT_FAMILY)
|
|
1314
|
+
.attr('font-size', sEdgeLabelFontSize)
|
|
1315
|
+
.attr('fill', palette.textMuted);
|
|
1316
|
+
const firstY =
|
|
1317
|
+
ly - ((lines.length - 1) / 2) * labelLineHeight + sEdgeLabelFontSize / 3;
|
|
1318
|
+
lines.forEach((line, k) => {
|
|
1319
|
+
text
|
|
1320
|
+
.append('tspan')
|
|
1321
|
+
.attr('x', lx)
|
|
1322
|
+
.attr('y', firstY + k * labelLineHeight)
|
|
1323
|
+
.text(line);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1399
1327
|
// ── Render legend ──────────────────────────────────────
|
|
1400
1328
|
const hasDescriptions = parsed.nodes.some(
|
|
1401
1329
|
(n) => n.description && n.description.length > 0
|
package/src/c4/parser.ts
CHANGED
|
@@ -516,7 +516,7 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
516
516
|
} else {
|
|
517
517
|
pushError(
|
|
518
518
|
lineNumber,
|
|
519
|
-
`"container ${refName}" must be
|
|
519
|
+
`"container ${refName}" must be indented under a deployment node (e.g. under 'Prod is a deployment-node')`
|
|
520
520
|
);
|
|
521
521
|
}
|
|
522
522
|
continue;
|
|
@@ -585,7 +585,10 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
585
585
|
indent,
|
|
586
586
|
});
|
|
587
587
|
} else {
|
|
588
|
-
pushError(
|
|
588
|
+
pushError(
|
|
589
|
+
lineNumber,
|
|
590
|
+
`"${sectionType}" must be indented under an element (e.g. under 'Web App is a container')`
|
|
591
|
+
);
|
|
589
592
|
}
|
|
590
593
|
continue;
|
|
591
594
|
}
|
|
@@ -618,7 +621,10 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
618
621
|
indent,
|
|
619
622
|
});
|
|
620
623
|
} else {
|
|
621
|
-
pushError(
|
|
624
|
+
pushError(
|
|
625
|
+
lineNumber,
|
|
626
|
+
`Group [${groupName}] must be indented under an element (e.g. under 'Web App is a container')`
|
|
627
|
+
);
|
|
622
628
|
}
|
|
623
629
|
continue;
|
|
624
630
|
}
|
|
@@ -1049,7 +1055,10 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
1049
1055
|
}
|
|
1050
1056
|
desc.push(descResult.text);
|
|
1051
1057
|
} else {
|
|
1052
|
-
pushError(
|
|
1058
|
+
pushError(
|
|
1059
|
+
lineNumber,
|
|
1060
|
+
`Unexpected content: "${trimmed}". Expected an element ('Web App is a container'), a relationship ('-Uses-> Database'), indented metadata ('tech: React'), or a group ('[Backend]'). (§8)`
|
|
1061
|
+
);
|
|
1053
1062
|
}
|
|
1054
1063
|
}
|
|
1055
1064
|
|
|
@@ -1154,7 +1163,7 @@ function validateRelationshipTargets(
|
|
|
1154
1163
|
for (const el of elements) {
|
|
1155
1164
|
for (const rel of el.relationships) {
|
|
1156
1165
|
if (!knownNames.has(normalizeName(rel.target))) {
|
|
1157
|
-
let msg = `Relationship target "${rel.target}" not found`;
|
|
1166
|
+
let msg = `Relationship target "${rel.target}" not found — declare it first (e.g. '${rel.target} is a container'), then reference it in the arrow`;
|
|
1158
1167
|
const hint = suggest(normalizeName(rel.target), allNames);
|
|
1159
1168
|
if (hint) msg += `. ${hint}`;
|
|
1160
1169
|
pushWarning(rel.lineNumber, msg, 'warning');
|
|
@@ -1193,7 +1202,7 @@ function validateDeploymentRefs(
|
|
|
1193
1202
|
for (const node of nodes) {
|
|
1194
1203
|
for (const ref of node.containerRefs) {
|
|
1195
1204
|
if (!knownNames.has(normalizeName(ref))) {
|
|
1196
|
-
let msg = `Deployment reference "container ${ref}" not found`;
|
|
1205
|
+
let msg = `Deployment reference "container ${ref}" not found — declare the container element first, then reference it under the deployment node`;
|
|
1197
1206
|
const hint = suggest(normalizeName(ref), allNames);
|
|
1198
1207
|
if (hint) msg += `. ${hint}`;
|
|
1199
1208
|
pushWarning(node.lineNumber, msg, 'warning');
|
package/src/chart.ts
CHANGED
|
@@ -174,7 +174,7 @@ export function parseChart(
|
|
|
174
174
|
result.diagnostics.push(
|
|
175
175
|
makeDgmoError(
|
|
176
176
|
lineNumber,
|
|
177
|
-
`'${trimmed}' — ##
|
|
177
|
+
`'${trimmed}' removed — use a bracket container '[${trimmed.replace(/^#+\s*/, '')}]' instead (## headers are no longer supported).`
|
|
178
178
|
)
|
|
179
179
|
);
|
|
180
180
|
continue;
|
package/src/class/parser.ts
CHANGED
|
@@ -308,7 +308,7 @@ export function parseClassDiagram(
|
|
|
308
308
|
result.diagnostics.push(
|
|
309
309
|
makeDgmoError(
|
|
310
310
|
lineNumber,
|
|
311
|
-
'"no-auto-color"
|
|
311
|
+
'"no-auto-color" was removed — colors are always auto-assigned from the palette; delete this line.',
|
|
312
312
|
'warning'
|
|
313
313
|
)
|
|
314
314
|
);
|
|
@@ -458,7 +458,11 @@ export function parseClassDiagram(
|
|
|
458
458
|
|
|
459
459
|
// Catch-all: nothing matched this line
|
|
460
460
|
result.diagnostics.push(
|
|
461
|
-
makeDgmoError(
|
|
461
|
+
makeDgmoError(
|
|
462
|
+
lineNumber,
|
|
463
|
+
`Unexpected line: '${trimmed}'. Classes start uppercase at indent 0 ('Ship'); members are indented and colon-typed ('+ name: string', '+ sail(): void'); relationships are indented arrows ('--|> Vessel'). (§10)`,
|
|
464
|
+
'warning'
|
|
465
|
+
)
|
|
462
466
|
);
|
|
463
467
|
}
|
|
464
468
|
|
package/src/cli-banner.ts
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
// sampled from the default `slate` palette (corporate blue → teal →
|
|
8
8
|
// steel cyan) so the wordmark matches the brand.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// clean.
|
|
10
|
+
// Color only when stdout is a TTY and NO_COLOR is unset; otherwise prints
|
|
11
|
+
// plain ASCII so pipes/CI stay clean.
|
|
13
12
|
|
|
14
13
|
import { getPalette } from './palettes';
|
|
15
14
|
|