@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.
Files changed (74) hide show
  1. package/dist/cli.cjs +92 -131
  2. package/dist/editor.cjs +13 -1
  3. package/dist/editor.cjs.map +1 -1
  4. package/dist/editor.js +13 -1
  5. package/dist/editor.js.map +1 -1
  6. package/dist/highlight.cjs +13 -1
  7. package/dist/highlight.cjs.map +1 -1
  8. package/dist/highlight.js +13 -1
  9. package/dist/highlight.js.map +1 -1
  10. package/dist/index.cjs +4524 -1511
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +427 -186
  13. package/dist/index.d.ts +427 -186
  14. package/dist/index.js +4526 -1503
  15. package/dist/index.js.map +1 -1
  16. package/docs/guide/chart-mindmap.md +198 -0
  17. package/docs/guide/chart-sequence.md +23 -1
  18. package/docs/guide/chart-wireframe.md +100 -0
  19. package/docs/guide/index.md +8 -0
  20. package/docs/language-reference.md +210 -2
  21. package/package.json +22 -9
  22. package/src/boxes-and-lines/collapse.ts +21 -3
  23. package/src/boxes-and-lines/layout.ts +51 -9
  24. package/src/boxes-and-lines/parser.ts +16 -4
  25. package/src/boxes-and-lines/renderer.ts +121 -23
  26. package/src/boxes-and-lines/types.ts +1 -0
  27. package/src/c4/parser.ts +8 -7
  28. package/src/class/parser.ts +6 -0
  29. package/src/cli.ts +1 -9
  30. package/src/completion.ts +26 -0
  31. package/src/d3.ts +169 -266
  32. package/src/dgmo-router.ts +103 -5
  33. package/src/diagnostics.ts +16 -6
  34. package/src/echarts.ts +43 -10
  35. package/src/editor/keywords.ts +12 -0
  36. package/src/er/parser.ts +22 -2
  37. package/src/gantt/renderer.ts +2 -2
  38. package/src/graph/flowchart-parser.ts +89 -52
  39. package/src/graph/layout.ts +73 -9
  40. package/src/graph/state-collapse.ts +78 -0
  41. package/src/graph/state-parser.ts +60 -35
  42. package/src/graph/state-renderer.ts +139 -34
  43. package/src/index.ts +41 -16
  44. package/src/infra/parser.ts +9 -2
  45. package/src/kanban/renderer.ts +305 -59
  46. package/src/mindmap/collapse.ts +88 -0
  47. package/src/mindmap/layout.ts +605 -0
  48. package/src/mindmap/parser.ts +379 -0
  49. package/src/mindmap/renderer.ts +543 -0
  50. package/src/mindmap/text-wrap.ts +207 -0
  51. package/src/mindmap/types.ts +55 -0
  52. package/src/palettes/color-utils.ts +4 -12
  53. package/src/palettes/index.ts +0 -4
  54. package/src/render.ts +31 -20
  55. package/src/sequence/parser.ts +7 -2
  56. package/src/sequence/renderer.ts +141 -21
  57. package/src/sharing.ts +2 -0
  58. package/src/sitemap/layout.ts +35 -12
  59. package/src/sitemap/renderer.ts +1 -6
  60. package/src/utils/arrows.ts +180 -11
  61. package/src/utils/d3-types.ts +4 -0
  62. package/src/utils/export-container.ts +3 -2
  63. package/src/utils/legend-constants.ts +0 -4
  64. package/src/utils/legend-d3.ts +1 -0
  65. package/src/utils/legend-layout.ts +2 -2
  66. package/src/utils/parsing.ts +2 -0
  67. package/src/utils/time-ticks.ts +213 -0
  68. package/src/wireframe/layout.ts +460 -0
  69. package/src/wireframe/parser.ts +956 -0
  70. package/src/wireframe/renderer.ts +1293 -0
  71. package/src/wireframe/types.ts +110 -0
  72. package/src/branding.ts +0 -67
  73. package/src/dgmo-mermaid.ts +0 -262
  74. 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 = 12;
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
- collapsedGroupLabels.add(og.label);
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 = Math.min(
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 = 1;
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 label = biLabeledMatch[2].trim();
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: label || undefined,
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 label = labeledMatch[2].trim();
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.curveMonotoneX);
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.curveMonotoneY);
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
- for (const group of layout.groups) {
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
- const gy = group.y - group.height / 2;
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', group.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
- // Apply parallel y-offset to points
520
- const points = le.points.map((p) => ({ x: p.x, y: p.y + le.yOffset }));
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 = le.deferred
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: le.labelY + le.yOffset,
650
+ y: ly,
555
651
  width: lw + 8,
556
- height: EDGE_LABEL_FONT_SIZE + 6,
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
  }
@@ -22,6 +22,7 @@ export interface BLGroup {
22
22
  children: string[];
23
23
  lineNumber: number;
24
24
  metadata: Record<string, string>;
25
+ parentGroup?: string;
25
26
  }
26
27
 
27
28
  export interface ParsedBoxesAndLines {
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
- // Extract [technology] from end of label
523
- let label: string | undefined = rawLabel;
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;
@@ -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
  // ============================================================