@diagrammo/dgmo 0.8.19 → 0.8.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/dist/cli.cjs +92 -131
- package/dist/editor.cjs +13 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +13 -1
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +13 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +13 -1
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +4524 -1511
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +427 -186
- package/dist/index.d.ts +427 -186
- package/dist/index.js +4526 -1503
- package/dist/index.js.map +1 -1
- package/docs/guide/chart-mindmap.md +198 -0
- package/docs/guide/chart-sequence.md +23 -1
- package/docs/guide/chart-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/language-reference.md +210 -2
- package/package.json +22 -9
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +51 -9
- package/src/boxes-and-lines/parser.ts +16 -4
- package/src/boxes-and-lines/renderer.ts +121 -23
- package/src/boxes-and-lines/types.ts +1 -0
- package/src/c4/parser.ts +8 -7
- package/src/class/parser.ts +6 -0
- package/src/cli.ts +1 -9
- package/src/completion.ts +26 -0
- package/src/d3.ts +169 -266
- package/src/dgmo-router.ts +103 -5
- package/src/diagnostics.ts +16 -6
- package/src/echarts.ts +43 -10
- package/src/editor/keywords.ts +12 -0
- package/src/er/parser.ts +22 -2
- package/src/gantt/renderer.ts +2 -2
- package/src/graph/flowchart-parser.ts +89 -52
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-parser.ts +60 -35
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +41 -16
- package/src/infra/parser.ts +9 -2
- package/src/kanban/renderer.ts +305 -59
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +379 -0
- package/src/mindmap/renderer.ts +543 -0
- package/src/mindmap/text-wrap.ts +207 -0
- package/src/mindmap/types.ts +55 -0
- package/src/palettes/color-utils.ts +4 -12
- package/src/palettes/index.ts +0 -4
- package/src/render.ts +31 -20
- package/src/sequence/parser.ts +7 -2
- package/src/sequence/renderer.ts +141 -21
- package/src/sharing.ts +2 -0
- package/src/sitemap/layout.ts +35 -12
- package/src/sitemap/renderer.ts +1 -6
- package/src/utils/arrows.ts +180 -11
- package/src/utils/d3-types.ts +4 -0
- package/src/utils/export-container.ts +3 -2
- package/src/utils/legend-constants.ts +0 -4
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +2 -2
- package/src/utils/parsing.ts +2 -0
- package/src/utils/time-ticks.ts +213 -0
- package/src/wireframe/layout.ts +460 -0
- package/src/wireframe/parser.ts +956 -0
- package/src/wireframe/renderer.ts +1293 -0
- package/src/wireframe/types.ts +110 -0
- package/src/branding.ts +0 -67
- package/src/dgmo-mermaid.ts +0 -262
- package/src/palettes/mermaid-bridge.ts +0 -220
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import dagre from '@dagrejs/dagre';
|
|
6
|
-
import type { ParsedBoxesAndLines, BLNode } from './types';
|
|
6
|
+
import type { ParsedBoxesAndLines, BLNode, BLGroup } from './types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Clip a point at (cx, cy) to the border of a rectangle centered at (cx, cy)
|
|
@@ -38,8 +38,7 @@ const CONTAINER_PAD_X = 30;
|
|
|
38
38
|
const CONTAINER_PAD_TOP = 40;
|
|
39
39
|
const CONTAINER_PAD_BOTTOM = 24;
|
|
40
40
|
const MAX_PARALLEL_EDGES = 5;
|
|
41
|
-
const PARALLEL_SPACING =
|
|
42
|
-
const PARALLEL_EDGE_MARGIN = 10;
|
|
41
|
+
const PARALLEL_SPACING = 22;
|
|
43
42
|
|
|
44
43
|
// ── Result types ───────────────────────────────────────────
|
|
45
44
|
|
|
@@ -116,12 +115,23 @@ export function layoutBoxesAndLines(
|
|
|
116
115
|
});
|
|
117
116
|
g.setDefaultEdgeLabel(() => ({}));
|
|
118
117
|
|
|
119
|
-
// Determine which groups are collapsed
|
|
118
|
+
// Determine which groups are collapsed (but not hidden inside a collapsed parent)
|
|
120
119
|
const collapsedGroupLabels = new Set<string>();
|
|
121
120
|
if (collapseInfo) {
|
|
121
|
+
// Build set of all groups that are missing from parsed (collapsed or hidden)
|
|
122
|
+
const missingGroups = new Set<string>();
|
|
122
123
|
for (const og of collapseInfo.originalGroups) {
|
|
123
124
|
if (!parsed.groups.some((g) => g.label === og.label)) {
|
|
124
|
-
|
|
125
|
+
missingGroups.add(og.label);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Only show a collapsed group as a node if its parent is NOT also missing
|
|
129
|
+
// (i.e., it's a directly collapsed group, not one hidden inside a collapsed parent)
|
|
130
|
+
for (const label of missingGroups) {
|
|
131
|
+
const og = collapseInfo.originalGroups.find((g) => g.label === label);
|
|
132
|
+
const parentLabel = og?.parentGroup;
|
|
133
|
+
if (!parentLabel || !missingGroups.has(parentLabel)) {
|
|
134
|
+
collapsedGroupLabels.add(label);
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
}
|
|
@@ -147,6 +157,25 @@ export function layoutBoxesAndLines(
|
|
|
147
157
|
});
|
|
148
158
|
}
|
|
149
159
|
|
|
160
|
+
// Re-establish parent relationships for collapsed groups
|
|
161
|
+
// (must run AFTER expanded groups are added to the graph)
|
|
162
|
+
const originalGroupByLabel = new Map<string, BLGroup>();
|
|
163
|
+
if (collapseInfo) {
|
|
164
|
+
for (const og of collapseInfo.originalGroups) {
|
|
165
|
+
originalGroupByLabel.set(og.label, og);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const label of collapsedGroupLabels) {
|
|
169
|
+
const og = originalGroupByLabel.get(label);
|
|
170
|
+
if (og?.parentGroup && !collapsedGroupLabels.has(og.parentGroup)) {
|
|
171
|
+
const gid = `__group_${label}`;
|
|
172
|
+
const parentGid = `__group_${og.parentGroup}`;
|
|
173
|
+
if (g.hasNode(parentGid)) {
|
|
174
|
+
g.setParent(gid, parentGid);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
// Add nodes
|
|
151
180
|
for (const node of parsed.nodes) {
|
|
152
181
|
const size = computeNodeSize(node);
|
|
@@ -157,10 +186,26 @@ export function layoutBoxesAndLines(
|
|
|
157
186
|
});
|
|
158
187
|
}
|
|
159
188
|
|
|
189
|
+
// Set parent relationships for nested groups
|
|
190
|
+
for (const group of parsed.groups) {
|
|
191
|
+
if (group.parentGroup) {
|
|
192
|
+
const childGid = `__group_${group.label}`;
|
|
193
|
+
const parentGid = `__group_${group.parentGroup}`;
|
|
194
|
+
if (g.hasNode(childGid) && g.hasNode(parentGid)) {
|
|
195
|
+
g.setParent(childGid, parentGid);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Build set of group labels for skip-check below
|
|
201
|
+
const groupLabelSet = new Set(parsed.groups.map((gr) => gr.label));
|
|
202
|
+
|
|
160
203
|
// Set parent relationships for nodes in groups
|
|
161
204
|
for (const group of parsed.groups) {
|
|
162
205
|
const gid = `__group_${group.label}`;
|
|
163
206
|
for (const child of group.children) {
|
|
207
|
+
// Skip children that are sub-groups — their parent is set above
|
|
208
|
+
if (groupLabelSet.has(child)) continue;
|
|
164
209
|
if (g.hasNode(child)) {
|
|
165
210
|
g.setParent(child, gid);
|
|
166
211
|
}
|
|
@@ -263,10 +308,7 @@ export function layoutBoxesAndLines(
|
|
|
263
308
|
edgeParallelCounts[idx] = 0;
|
|
264
309
|
}
|
|
265
310
|
if (capped.length < 2) continue;
|
|
266
|
-
const effectiveSpacing =
|
|
267
|
-
PARALLEL_SPACING,
|
|
268
|
-
(60 - PARALLEL_EDGE_MARGIN) / (capped.length - 1)
|
|
269
|
-
);
|
|
311
|
+
const effectiveSpacing = PARALLEL_SPACING;
|
|
270
312
|
for (let j = 0; j < capped.length; j++) {
|
|
271
313
|
edgeYOffsets[capped[j]] =
|
|
272
314
|
(j - (capped.length - 1) / 2) * effectiveSpacing;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { makeDgmoError, suggest } from '../diagnostics';
|
|
6
6
|
import type { DgmoError } from '../diagnostics';
|
|
7
|
+
import { parseInArrowLabel } from '../utils/arrows';
|
|
7
8
|
import type { ParsedBoxesAndLines, BLNode, BLEdge, BLGroup } from './types';
|
|
8
9
|
import {
|
|
9
10
|
matchTagBlockHeading,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
OPTION_NOCOLON_RE,
|
|
20
21
|
} from '../utils/parsing';
|
|
21
22
|
|
|
22
|
-
const MAX_GROUP_DEPTH =
|
|
23
|
+
const MAX_GROUP_DEPTH = 2;
|
|
23
24
|
|
|
24
25
|
/** Boxes-and-lines requires explicit first line — no heuristic detection. */
|
|
25
26
|
export function looksLikeBoxesAndLines(_content: string): boolean {
|
|
@@ -386,13 +387,20 @@ export function parseBoxesAndLines(content: string): ParsedBoxesAndLines {
|
|
|
386
387
|
}
|
|
387
388
|
}
|
|
388
389
|
|
|
390
|
+
const parentGs = currentGroupState();
|
|
389
391
|
const group: BLGroup = {
|
|
390
392
|
label,
|
|
391
393
|
children: [],
|
|
392
394
|
lineNumber: lineNum,
|
|
393
395
|
metadata: groupMeta,
|
|
396
|
+
...(parentGs ? { parentGroup: parentGs.group.label } : {}),
|
|
394
397
|
};
|
|
395
398
|
|
|
399
|
+
// Add nested group as child of parent group
|
|
400
|
+
if (parentGs && indent > parentGs.indent) {
|
|
401
|
+
parentGs.group.children.push(label);
|
|
402
|
+
}
|
|
403
|
+
|
|
396
404
|
groupLabels.add(label);
|
|
397
405
|
groupStack.push({ group, indent, depth: currentDepth });
|
|
398
406
|
lastNodeLabel = label;
|
|
@@ -607,7 +615,9 @@ function parseEdgeLine(
|
|
|
607
615
|
const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
|
|
608
616
|
if (biLabeledMatch) {
|
|
609
617
|
const source = resolveEndpoint(biLabeledMatch[1].trim());
|
|
610
|
-
const
|
|
618
|
+
const labelResult = parseInArrowLabel(biLabeledMatch[2], lineNum);
|
|
619
|
+
diagnostics.push(...labelResult.diagnostics);
|
|
620
|
+
const label = labelResult.label;
|
|
611
621
|
let rest = biLabeledMatch[3].trim();
|
|
612
622
|
|
|
613
623
|
let metadata: Record<string, string> = {};
|
|
@@ -631,7 +641,7 @@ function parseEdgeLine(
|
|
|
631
641
|
return {
|
|
632
642
|
source,
|
|
633
643
|
target: resolveEndpoint(rest),
|
|
634
|
-
label
|
|
644
|
+
label,
|
|
635
645
|
bidirectional: true,
|
|
636
646
|
lineNumber: lineNum,
|
|
637
647
|
metadata,
|
|
@@ -675,7 +685,9 @@ function parseEdgeLine(
|
|
|
675
685
|
const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
|
|
676
686
|
if (labeledMatch) {
|
|
677
687
|
const source = resolveEndpoint(labeledMatch[1].trim());
|
|
678
|
-
const
|
|
688
|
+
const labelResult = parseInArrowLabel(labeledMatch[2], lineNum);
|
|
689
|
+
diagnostics.push(...labelResult.diagnostics);
|
|
690
|
+
const label = labelResult.label;
|
|
679
691
|
let rest = labeledMatch[3].trim();
|
|
680
692
|
|
|
681
693
|
if (label) {
|
|
@@ -36,6 +36,7 @@ const CHAR_WIDTH_RATIO = 0.6;
|
|
|
36
36
|
const NODE_TEXT_PADDING = 12;
|
|
37
37
|
const GROUP_RX = 8;
|
|
38
38
|
const GROUP_LABEL_FONT_SIZE = 14;
|
|
39
|
+
const GROUP_LABEL_ZONE = 32;
|
|
39
40
|
|
|
40
41
|
type D3G = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
41
42
|
type D3Svg = d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
|
|
@@ -45,19 +46,13 @@ const lineGeneratorLR = d3Shape
|
|
|
45
46
|
.line<{ x: number; y: number }>()
|
|
46
47
|
.x((d) => d.x)
|
|
47
48
|
.y((d) => d.y)
|
|
48
|
-
.curve(d3Shape.
|
|
49
|
+
.curve(d3Shape.curveBasis);
|
|
49
50
|
|
|
50
51
|
const lineGeneratorTB = d3Shape
|
|
51
52
|
.line<{ x: number; y: number }>()
|
|
52
53
|
.x((d) => d.x)
|
|
53
54
|
.y((d) => d.y)
|
|
54
|
-
.curve(d3Shape.
|
|
55
|
-
|
|
56
|
-
const lineGeneratorLinear = d3Shape
|
|
57
|
-
.line<{ x: number; y: number }>()
|
|
58
|
-
.x((d) => d.x)
|
|
59
|
-
.y((d) => d.y)
|
|
60
|
-
.curve(d3Shape.curveLinear);
|
|
55
|
+
.curve(d3Shape.curveBasis);
|
|
61
56
|
|
|
62
57
|
// ── Text fitting ───────────────────────────────────────────
|
|
63
58
|
|
|
@@ -336,8 +331,18 @@ export function renderBoxesAndLines(
|
|
|
336
331
|
// Compute diagram bounds for scaling
|
|
337
332
|
const titleOffset = parsed.title ? 40 : 0;
|
|
338
333
|
const legendH = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + 8 : 0;
|
|
334
|
+
|
|
335
|
+
// Account for group label zone extensions (renderer-only, not in layout.height)
|
|
336
|
+
const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
|
|
337
|
+
let labelZoneExtension = 0;
|
|
338
|
+
for (const group of parsed.groups) {
|
|
339
|
+
if (group.children.some((c) => groupLabelsSet.has(c))) {
|
|
340
|
+
labelZoneExtension += GROUP_LABEL_ZONE;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
339
344
|
const contentW = layout.width;
|
|
340
|
-
const contentH = layout.height + titleOffset + legendH;
|
|
345
|
+
const contentH = layout.height + titleOffset + legendH + labelZoneExtension;
|
|
341
346
|
|
|
342
347
|
const scaleX = width / (contentW + DIAGRAM_PADDING * 2);
|
|
343
348
|
const scaleY = height / (contentH + DIAGRAM_PADDING * 2);
|
|
@@ -390,10 +395,29 @@ export function renderBoxesAndLines(
|
|
|
390
395
|
}
|
|
391
396
|
ensureArrowMarkers(defs, arrowColors);
|
|
392
397
|
|
|
393
|
-
// ── Render groups (bottom layer)
|
|
394
|
-
|
|
398
|
+
// ── Render groups (bottom layer, largest first for nesting) ──
|
|
399
|
+
const sortedGroups = [...layout.groups].sort(
|
|
400
|
+
(a, b) => b.width * b.height - a.width * a.height
|
|
401
|
+
);
|
|
402
|
+
// Identify groups that contain sub-groups — only those need extra label space
|
|
403
|
+
const groupLabels = new Set(layout.groups.map((g) => g.label));
|
|
404
|
+
const hasSubGroups = new Set<string>();
|
|
405
|
+
for (const group of parsed.groups) {
|
|
406
|
+
for (const child of group.children) {
|
|
407
|
+
if (groupLabels.has(child)) hasSubGroups.add(group.label);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (const group of sortedGroups) {
|
|
395
412
|
const gx = group.x - group.width / 2;
|
|
396
|
-
|
|
413
|
+
// Only extend top for groups that contain sub-groups (dagre under-pads these)
|
|
414
|
+
const needsExtra = !group.collapsed && hasSubGroups.has(group.label);
|
|
415
|
+
const gy = needsExtra
|
|
416
|
+
? group.y - group.height / 2 - GROUP_LABEL_ZONE
|
|
417
|
+
: group.y - group.height / 2;
|
|
418
|
+
const groupHeight = needsExtra
|
|
419
|
+
? group.height + GROUP_LABEL_ZONE
|
|
420
|
+
: group.height;
|
|
397
421
|
|
|
398
422
|
const groupG = diagramG
|
|
399
423
|
.append('g')
|
|
@@ -464,7 +488,7 @@ export function renderBoxesAndLines(
|
|
|
464
488
|
.attr('x', gx)
|
|
465
489
|
.attr('y', gy)
|
|
466
490
|
.attr('width', group.width)
|
|
467
|
-
.attr('height',
|
|
491
|
+
.attr('height', groupHeight)
|
|
468
492
|
.attr('rx', GROUP_RX)
|
|
469
493
|
.attr('ry', GROUP_RX)
|
|
470
494
|
.attr('fill', mix(palette.surface, palette.bg, 40))
|
|
@@ -516,8 +540,73 @@ export function renderBoxesAndLines(
|
|
|
516
540
|
if (isHidden) continue;
|
|
517
541
|
}
|
|
518
542
|
|
|
519
|
-
//
|
|
520
|
-
|
|
543
|
+
// Self-loop: render as a smooth circular arc below the node
|
|
544
|
+
if (le.source === le.target) {
|
|
545
|
+
const nodeLayout = layoutNodeMap.get(le.source);
|
|
546
|
+
if (nodeLayout) {
|
|
547
|
+
const edgeG = diagramG
|
|
548
|
+
.append('g')
|
|
549
|
+
.attr('class', 'bl-edge-group')
|
|
550
|
+
.attr('data-line-number', String(le.lineNumber));
|
|
551
|
+
edgeGroups.set(i, edgeG as unknown as D3G);
|
|
552
|
+
|
|
553
|
+
const markerId = `bl-arrow-${color.replace('#', '')}`;
|
|
554
|
+
const cx = nodeLayout.x;
|
|
555
|
+
const cy = nodeLayout.y;
|
|
556
|
+
const hw = nodeLayout.width / 2;
|
|
557
|
+
const hh = nodeLayout.height / 2;
|
|
558
|
+
const pad = 20; // clearance from node edge
|
|
559
|
+
|
|
560
|
+
// Arc exits from bottom of right side, swings wide, returns to right of bottom side
|
|
561
|
+
const startX = cx + hw;
|
|
562
|
+
const startY = cy + hh * 0.4;
|
|
563
|
+
const endX = cx + hw * 0.4;
|
|
564
|
+
const endY = cy + hh;
|
|
565
|
+
|
|
566
|
+
// Control points swing far out to create a smooth circular arc
|
|
567
|
+
const cp1x = startX + hw + pad;
|
|
568
|
+
const cp1y = startY;
|
|
569
|
+
const cp2x = endX;
|
|
570
|
+
const cp2y = endY + hh + pad;
|
|
571
|
+
|
|
572
|
+
edgeG
|
|
573
|
+
.append('path')
|
|
574
|
+
.attr('class', 'bl-edge')
|
|
575
|
+
.attr(
|
|
576
|
+
'd',
|
|
577
|
+
`M ${startX} ${startY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${endX} ${endY}`
|
|
578
|
+
)
|
|
579
|
+
.attr('fill', 'none')
|
|
580
|
+
.attr('stroke', color)
|
|
581
|
+
.attr('stroke-width', EDGE_STROKE_WIDTH)
|
|
582
|
+
.attr('marker-end', `url(#${markerId})`);
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Parallel edge fan: construct explicit 5-point geometry so lines
|
|
588
|
+
// bundle at ports and visibly spread apart in the middle.
|
|
589
|
+
let points: { x: number; y: number }[];
|
|
590
|
+
if (le.yOffset !== 0 && le.parallelCount > 1) {
|
|
591
|
+
const srcLayout = layoutNodeMap.get(le.source);
|
|
592
|
+
const tgtLayout = layoutNodeMap.get(le.target);
|
|
593
|
+
const srcY = srcLayout?.y ?? le.points[0]?.y ?? 0;
|
|
594
|
+
const tgtY = tgtLayout?.y ?? le.points[le.points.length - 1]?.y ?? 0;
|
|
595
|
+
const srcX = le.points[0]?.x ?? 0;
|
|
596
|
+
const tgtX = le.points[le.points.length - 1]?.x ?? 0;
|
|
597
|
+
const midX = (srcX + tgtX) / 2;
|
|
598
|
+
const midY = (srcY + tgtY) / 2;
|
|
599
|
+
|
|
600
|
+
points = [
|
|
601
|
+
{ x: srcX, y: srcY }, // port (bundled)
|
|
602
|
+
{ x: srcX + (midX - srcX) * 0.3, y: srcY + le.yOffset * 0.5 }, // separate
|
|
603
|
+
{ x: midX, y: midY + le.yOffset }, // full spread
|
|
604
|
+
{ x: tgtX - (tgtX - midX) * 0.3, y: tgtY + le.yOffset * 0.5 }, // converge
|
|
605
|
+
{ x: tgtX, y: tgtY }, // port (bundled)
|
|
606
|
+
];
|
|
607
|
+
} else {
|
|
608
|
+
points = le.points.map((p) => ({ x: p.x, y: p.y }));
|
|
609
|
+
}
|
|
521
610
|
if (points.length < 2) continue;
|
|
522
611
|
|
|
523
612
|
const edgeG = diagramG
|
|
@@ -527,11 +616,7 @@ export function renderBoxesAndLines(
|
|
|
527
616
|
edgeGroups.set(i, edgeG as unknown as D3G);
|
|
528
617
|
|
|
529
618
|
const markerId = `bl-arrow-${color.replace('#', '')}`;
|
|
530
|
-
const gen =
|
|
531
|
-
? lineGeneratorLinear
|
|
532
|
-
: parsed.direction === 'TB'
|
|
533
|
-
? lineGeneratorTB
|
|
534
|
-
: lineGeneratorLR;
|
|
619
|
+
const gen = parsed.direction === 'TB' ? lineGeneratorTB : lineGeneratorLR;
|
|
535
620
|
const path = edgeG
|
|
536
621
|
.append('path')
|
|
537
622
|
.attr('class', 'bl-edge')
|
|
@@ -546,14 +631,25 @@ export function renderBoxesAndLines(
|
|
|
546
631
|
path.attr('marker-start', `url(#${revId})`);
|
|
547
632
|
}
|
|
548
633
|
|
|
549
|
-
// Edge label
|
|
634
|
+
// Edge label — for parallel edges, place relative to each line:
|
|
635
|
+
// negative offset (top line) → label above, zero → on line, positive → below
|
|
550
636
|
if (le.label && le.labelX != null && le.labelY != null) {
|
|
551
637
|
const lw = le.label.length * EDGE_LABEL_FONT_SIZE * CHAR_WIDTH_RATIO;
|
|
638
|
+
const labelH = EDGE_LABEL_FONT_SIZE + 6;
|
|
639
|
+
let ly: number;
|
|
640
|
+
if (le.parallelCount > 1 && le.yOffset !== 0) {
|
|
641
|
+
// Position label on the line at midpoint, shifted above/below based on offset sign
|
|
642
|
+
const lineY = le.labelY + 10 + le.yOffset; // +10 to undo the -10 in layout
|
|
643
|
+
const labelShift = le.yOffset < 0 ? -labelH : labelH;
|
|
644
|
+
ly = lineY + labelShift * 0.5;
|
|
645
|
+
} else {
|
|
646
|
+
ly = le.labelY + le.yOffset;
|
|
647
|
+
}
|
|
552
648
|
labelPositions.push({
|
|
553
649
|
x: le.labelX,
|
|
554
|
-
y:
|
|
650
|
+
y: ly,
|
|
555
651
|
width: lw + 8,
|
|
556
|
-
height:
|
|
652
|
+
height: labelH,
|
|
557
653
|
idx: i,
|
|
558
654
|
});
|
|
559
655
|
}
|
|
@@ -743,10 +839,12 @@ export function renderBoxesAndLinesForExport(
|
|
|
743
839
|
options?: {
|
|
744
840
|
exportDims?: { width: number; height: number };
|
|
745
841
|
activeTagGroup?: string | null;
|
|
842
|
+
hiddenTagValues?: Map<string, Set<string>>;
|
|
746
843
|
}
|
|
747
844
|
): void {
|
|
748
845
|
renderBoxesAndLines(container, parsed, layout, palette, isDark, {
|
|
749
846
|
exportDims: options?.exportDims,
|
|
750
847
|
activeTagGroup: options?.activeTagGroup,
|
|
848
|
+
hiddenTagValues: options?.hiddenTagValues,
|
|
751
849
|
});
|
|
752
850
|
}
|
package/src/c4/parser.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { PaletteColors } from '../palettes';
|
|
6
6
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
7
|
+
import { parseInArrowLabel } from '../utils/arrows';
|
|
7
8
|
import type { TagGroup } from '../utils/tag-groups';
|
|
8
9
|
import {
|
|
9
10
|
matchTagBlockHeading,
|
|
@@ -519,14 +520,14 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
519
520
|
break;
|
|
520
521
|
}
|
|
521
522
|
|
|
522
|
-
//
|
|
523
|
-
|
|
523
|
+
// TD-5: the trailing `[tech]` sugar is no longer extracted from the
|
|
524
|
+
// in-arrow label. The entire label stays as the label. Technology
|
|
525
|
+
// metadata comes from post-colon or pipe metadata on the target.
|
|
526
|
+
// Also run TD-13/TD-14 validation on the label characters.
|
|
527
|
+
const labelResult = parseInArrowLabel(rawLabel, lineNumber);
|
|
528
|
+
labelResult.diagnostics.forEach((d) => result.diagnostics.push(d));
|
|
529
|
+
const label: string | undefined = labelResult.label;
|
|
524
530
|
let technology: string | undefined;
|
|
525
|
-
const techMatch = rawLabel.match(/\[([^\]]+)\]\s*$/);
|
|
526
|
-
if (techMatch) {
|
|
527
|
-
label = rawLabel.substring(0, techMatch.index!).trim() || undefined;
|
|
528
|
-
technology = techMatch[1].trim();
|
|
529
|
-
}
|
|
530
531
|
|
|
531
532
|
// Extract pipe metadata from target body (e.g. "Database | tech: SQL")
|
|
532
533
|
let target = targetBody;
|
package/src/class/parser.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveColorWithDiagnostic } from '../colors';
|
|
2
2
|
import type { PaletteColors } from '../palettes';
|
|
3
3
|
import { makeDgmoError, formatDgmoError } from '../diagnostics';
|
|
4
|
+
import { validateLabelCharacters } from '../utils/arrows';
|
|
4
5
|
import {
|
|
5
6
|
measureIndent,
|
|
6
7
|
parseFirstLine,
|
|
@@ -251,6 +252,11 @@ export function parseClassDiagram(
|
|
|
251
252
|
|
|
252
253
|
getOrCreateClass(targetName, lineNumber);
|
|
253
254
|
|
|
255
|
+
if (label) {
|
|
256
|
+
result.diagnostics.push(
|
|
257
|
+
...validateLabelCharacters(label, lineNumber)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
254
260
|
result.relationships.push({
|
|
255
261
|
source: currentClass.id,
|
|
256
262
|
target: classId(targetName),
|
package/src/cli.ts
CHANGED
|
@@ -160,7 +160,6 @@ Key options:
|
|
|
160
160
|
- \`--theme <theme>\` — \`light\` (default), \`dark\`, \`transparent\`
|
|
161
161
|
- \`--palette <name>\` — \`nord\` (default), \`solarized\`, \`catppuccin\`, \`rose-pine\`, \`gruvbox\`, \`tokyo-night\`, \`one-dark\`, \`bold\`
|
|
162
162
|
- \`--copy\` — copy the URL to clipboard (use with \`-o url\`)
|
|
163
|
-
- \`--branding\` — add diagrammo.app branding to exports
|
|
164
163
|
- \`--chart-types\` — list all supported chart types
|
|
165
164
|
|
|
166
165
|
## Supported Chart Types
|
|
@@ -502,7 +501,6 @@ Options:
|
|
|
502
501
|
--c4-system <name> System to drill into (with --c4-level containers or components)
|
|
503
502
|
--c4-container <name> Container to drill into (with --c4-level components)
|
|
504
503
|
--tag-group <name> Pre-select a tag group for static export coloring
|
|
505
|
-
--branding Add diagrammo.app branding to exports
|
|
506
504
|
--copy Copy URL to clipboard (only with -o url)
|
|
507
505
|
--json Output structured JSON to stdout
|
|
508
506
|
--chart-types List all supported chart types
|
|
@@ -532,7 +530,6 @@ function parseArgs(argv: string[]): {
|
|
|
532
530
|
palette: string;
|
|
533
531
|
help: boolean;
|
|
534
532
|
version: boolean;
|
|
535
|
-
branding: boolean;
|
|
536
533
|
copy: boolean;
|
|
537
534
|
json: boolean;
|
|
538
535
|
chartTypes: boolean;
|
|
@@ -553,7 +550,6 @@ function parseArgs(argv: string[]): {
|
|
|
553
550
|
palette: 'nord',
|
|
554
551
|
help: false,
|
|
555
552
|
version: false,
|
|
556
|
-
branding: false,
|
|
557
553
|
copy: false,
|
|
558
554
|
json: false,
|
|
559
555
|
chartTypes: false,
|
|
@@ -637,9 +633,6 @@ function parseArgs(argv: string[]): {
|
|
|
637
633
|
} else if (arg === '--tag-group') {
|
|
638
634
|
result.tagGroup = args[++i];
|
|
639
635
|
i++;
|
|
640
|
-
} else if (arg === '--branding') {
|
|
641
|
-
result.branding = true;
|
|
642
|
-
i++;
|
|
643
636
|
} else if (arg === '--json') {
|
|
644
637
|
result.json = true;
|
|
645
638
|
i++;
|
|
@@ -1251,10 +1244,9 @@ async function main(): Promise<void> {
|
|
|
1251
1244
|
}
|
|
1252
1245
|
}
|
|
1253
1246
|
|
|
1254
|
-
const svg = await render(content, {
|
|
1247
|
+
const { svg } = await render(content, {
|
|
1255
1248
|
theme: opts.theme,
|
|
1256
1249
|
palette: opts.palette,
|
|
1257
|
-
branding: opts.branding,
|
|
1258
1250
|
c4Level: opts.c4Level,
|
|
1259
1251
|
c4System: opts.c4System,
|
|
1260
1252
|
c4Container: opts.c4Container,
|
package/src/completion.ts
CHANGED
|
@@ -335,6 +335,20 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
335
335
|
hide: { description: 'Hide tag:value pairs' },
|
|
336
336
|
}),
|
|
337
337
|
],
|
|
338
|
+
[
|
|
339
|
+
'mindmap',
|
|
340
|
+
withGlobals({
|
|
341
|
+
'hide-descriptions': { description: 'Hide node descriptions' },
|
|
342
|
+
'active-tag': { description: 'Active tag group name' },
|
|
343
|
+
}),
|
|
344
|
+
],
|
|
345
|
+
[
|
|
346
|
+
'wireframe',
|
|
347
|
+
withGlobals({
|
|
348
|
+
mobile: { description: 'Use mobile (narrow vertical) layout' },
|
|
349
|
+
'active-tag': { description: 'Active tag group name' },
|
|
350
|
+
}),
|
|
351
|
+
],
|
|
338
352
|
]);
|
|
339
353
|
|
|
340
354
|
// ============================================================
|
|
@@ -378,6 +392,8 @@ const CHART_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
|
378
392
|
infra: 'Infrastructure diagram',
|
|
379
393
|
gantt: 'Gantt chart',
|
|
380
394
|
'boxes-and-lines': 'Boxes and lines diagram',
|
|
395
|
+
mindmap: 'Mindmap diagram',
|
|
396
|
+
wireframe: 'UI wireframe diagram',
|
|
381
397
|
};
|
|
382
398
|
|
|
383
399
|
/** All chart types with descriptions, for chart type autocomplete. Excludes `multi-line` alias. */
|
|
@@ -511,6 +527,16 @@ export const PIPE_METADATA = new Map<
|
|
|
511
527
|
edge: {},
|
|
512
528
|
},
|
|
513
529
|
],
|
|
530
|
+
[
|
|
531
|
+
'mindmap',
|
|
532
|
+
{
|
|
533
|
+
node: {
|
|
534
|
+
description: { description: 'Node description text' },
|
|
535
|
+
collapsed: { description: 'Collapse node subtree by default' },
|
|
536
|
+
},
|
|
537
|
+
edge: {},
|
|
538
|
+
},
|
|
539
|
+
],
|
|
514
540
|
]);
|
|
515
541
|
|
|
516
542
|
// ============================================================
|