@diagrammo/dgmo 0.8.9 → 0.8.11

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 (46) hide show
  1. package/AGENTS.md +3 -0
  2. package/dist/cli.cjs +245 -672
  3. package/dist/editor.cjs.map +1 -1
  4. package/dist/editor.d.cts +2 -3
  5. package/dist/editor.d.ts +2 -3
  6. package/dist/editor.js.map +1 -1
  7. package/dist/index.cjs +1623 -800
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +153 -1
  10. package/dist/index.d.ts +153 -1
  11. package/dist/index.js +1619 -802
  12. package/dist/index.js.map +1 -1
  13. package/docs/language-reference.md +28 -2
  14. package/gallery/fixtures/sitemap-full.dgmo +1 -0
  15. package/package.json +14 -17
  16. package/src/boxes-and-lines/layout.ts +48 -8
  17. package/src/boxes-and-lines/parser.ts +59 -13
  18. package/src/boxes-and-lines/renderer.ts +34 -138
  19. package/src/c4/layout.ts +31 -10
  20. package/src/c4/renderer.ts +25 -138
  21. package/src/class/renderer.ts +185 -186
  22. package/src/d3.ts +194 -222
  23. package/src/echarts.ts +56 -57
  24. package/src/editor/index.ts +1 -2
  25. package/src/er/renderer.ts +52 -245
  26. package/src/gantt/renderer.ts +140 -182
  27. package/src/gantt/resolver.ts +19 -14
  28. package/src/index.ts +23 -1
  29. package/src/infra/renderer.ts +91 -244
  30. package/src/kanban/renderer.ts +29 -133
  31. package/src/label-layout.ts +286 -0
  32. package/src/org/renderer.ts +103 -170
  33. package/src/render.ts +39 -9
  34. package/src/sequence/parser.ts +4 -0
  35. package/src/sequence/renderer.ts +47 -154
  36. package/src/sitemap/layout.ts +180 -38
  37. package/src/sitemap/parser.ts +64 -23
  38. package/src/sitemap/renderer.ts +73 -161
  39. package/src/utils/arrows.ts +1 -1
  40. package/src/utils/legend-constants.ts +6 -0
  41. package/src/utils/legend-d3.ts +400 -0
  42. package/src/utils/legend-layout.ts +491 -0
  43. package/src/utils/legend-svg.ts +28 -2
  44. package/src/utils/legend-types.ts +166 -0
  45. package/src/utils/parsing.ts +1 -1
  46. package/src/utils/tag-groups.ts +1 -1
@@ -91,6 +91,7 @@ export interface SequenceMessage {
91
91
  export interface ElseIfBranch {
92
92
  label: string;
93
93
  children: SequenceElement[];
94
+ lineNumber: number;
94
95
  }
95
96
 
96
97
  export interface SequenceBlock {
@@ -100,6 +101,7 @@ export interface SequenceBlock {
100
101
  children: SequenceElement[];
101
102
  elseChildren: SequenceElement[];
102
103
  elseIfBranches?: ElseIfBranch[];
104
+ elseLineNumber?: number;
103
105
  lineNumber: number;
104
106
  }
105
107
 
@@ -1114,6 +1116,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1114
1116
  const branch: ElseIfBranch = {
1115
1117
  label: elseIfMatch[1].trim(),
1116
1118
  children: [],
1119
+ lineNumber,
1117
1120
  };
1118
1121
  if (!top.block.elseIfBranches) top.block.elseIfBranches = [];
1119
1122
  top.block.elseIfBranches.push(branch);
@@ -1141,6 +1144,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
1141
1144
  if (top.block.type === 'if') {
1142
1145
  top.inElse = true;
1143
1146
  top.activeElseIfBranch = undefined;
1147
+ top.block.elseLineNumber = lineNumber;
1144
1148
  }
1145
1149
  }
1146
1150
  continue;
@@ -24,18 +24,9 @@ import type {
24
24
  import { isSequenceBlock, isSequenceSection, isSequenceNote } from './parser';
25
25
  import { resolveSequenceTags } from './tag-resolution';
26
26
  import type { ResolvedTagMap } from './tag-resolution';
27
- import {
28
- LEGEND_HEIGHT,
29
- LEGEND_PILL_PAD,
30
- LEGEND_PILL_FONT_SIZE,
31
- LEGEND_CAPSULE_PAD,
32
- LEGEND_DOT_R,
33
- LEGEND_ENTRY_FONT_SIZE,
34
- LEGEND_ENTRY_DOT_GAP,
35
- LEGEND_ENTRY_TRAIL,
36
- LEGEND_GROUP_GAP,
37
- measureLegendText,
38
- } from '../utils/legend-constants';
27
+ import { LEGEND_HEIGHT } from '../utils/legend-constants';
28
+ import { renderLegendD3 } from '../utils/legend-d3';
29
+ import type { LegendConfig, LegendState } from '../utils/legend-types';
39
30
  import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
40
31
 
41
32
  // ============================================================
@@ -1598,146 +1589,35 @@ export function renderSequenceDiagram(
1598
1589
  // Render legend pills for tag groups
1599
1590
  if (parsed.tagGroups.length > 0) {
1600
1591
  const legendY = TOP_MARGIN + titleOffset;
1601
- const groupBg = isDark
1602
- ? mix(palette.surface, palette.bg, 50)
1603
- : mix(palette.surface, palette.bg, 30);
1604
-
1605
- // Pre-compute pill/capsule widths for centering
1606
- const legendItems: Array<{
1607
- group: (typeof parsed.tagGroups)[0];
1608
- isActive: boolean;
1609
- pillWidth: number;
1610
- totalWidth: number;
1611
- entries: Array<{ value: string; color: string }>;
1612
- }> = [];
1613
- for (const tg of parsed.tagGroups) {
1614
- if (tg.entries.length === 0) continue;
1615
- const isActive =
1616
- !!activeTagGroup &&
1617
- tg.name.toLowerCase() === activeTagGroup.toLowerCase();
1618
- const pillWidth =
1619
- measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
1620
- const entries = tg.entries.map((e) => ({
1621
- value: e.value,
1622
- color: resolveColor(e.color) ?? e.color,
1592
+ // Resolve tag colors for legend entries
1593
+ const resolvedGroups = parsed.tagGroups
1594
+ .filter((tg) => tg.entries.length > 0)
1595
+ .map((tg) => ({
1596
+ name: tg.name,
1597
+ entries: tg.entries.map((e) => ({
1598
+ value: e.value,
1599
+ color: resolveColor(e.color) ?? e.color,
1600
+ })),
1623
1601
  }));
1624
- let totalWidth = pillWidth;
1625
- if (isActive) {
1626
- let entriesWidth = 0;
1627
- for (const entry of entries) {
1628
- entriesWidth +=
1629
- LEGEND_DOT_R * 2 +
1630
- LEGEND_ENTRY_DOT_GAP +
1631
- measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
1632
- LEGEND_ENTRY_TRAIL;
1633
- }
1634
- totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth;
1635
- }
1636
- legendItems.push({ group: tg, isActive, pillWidth, totalWidth, entries });
1637
- }
1638
-
1639
- // Center legend horizontally
1640
- const totalLegendWidth =
1641
- legendItems.reduce((s, item) => s + item.totalWidth, 0) +
1642
- (legendItems.length - 1) * LEGEND_GROUP_GAP;
1643
- let legendX = (svgWidth - totalLegendWidth) / 2;
1644
-
1645
- const legendContainer = svg.append('g').attr('class', 'sequence-legend');
1646
- if (activeTagGroup) {
1647
- legendContainer.attr('data-legend-active', activeTagGroup.toLowerCase());
1648
- }
1649
-
1650
- for (const item of legendItems) {
1651
- const gEl = legendContainer
1652
- .append('g')
1653
- .attr('transform', `translate(${legendX}, ${legendY})`)
1654
- .attr('class', 'sequence-legend-group')
1655
- .attr('data-legend-group', item.group.name.toLowerCase())
1656
- .style('cursor', 'pointer');
1657
-
1658
- // Outer capsule background (active only)
1659
- if (item.isActive) {
1660
- gEl
1661
- .append('rect')
1662
- .attr('width', item.totalWidth)
1663
- .attr('height', LEGEND_HEIGHT)
1664
- .attr('rx', LEGEND_HEIGHT / 2)
1665
- .attr('fill', groupBg);
1666
- }
1667
-
1668
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
1669
- const pillYOff = LEGEND_CAPSULE_PAD;
1670
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
1671
-
1672
- // Pill background
1673
- gEl
1674
- .append('rect')
1675
- .attr('x', pillXOff)
1676
- .attr('y', pillYOff)
1677
- .attr('width', item.pillWidth)
1678
- .attr('height', pillH)
1679
- .attr('rx', pillH / 2)
1680
- .attr('fill', item.isActive ? palette.bg : groupBg);
1681
-
1682
- // Active pill border
1683
- if (item.isActive) {
1684
- gEl
1685
- .append('rect')
1686
- .attr('x', pillXOff)
1687
- .attr('y', pillYOff)
1688
- .attr('width', item.pillWidth)
1689
- .attr('height', pillH)
1690
- .attr('rx', pillH / 2)
1691
- .attr('fill', 'none')
1692
- .attr('stroke', mix(palette.textMuted, palette.bg, 50))
1693
- .attr('stroke-width', 0.75);
1694
- }
1695
-
1696
- // Pill text
1697
- gEl
1698
- .append('text')
1699
- .attr('x', pillXOff + item.pillWidth / 2)
1700
- .attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
1701
- .attr('font-size', LEGEND_PILL_FONT_SIZE)
1702
- .attr('font-weight', '500')
1703
- .attr('fill', item.isActive ? palette.text : palette.textMuted)
1704
- .attr('text-anchor', 'middle')
1705
- .text(item.group.name);
1706
-
1707
- // Entries inside capsule (active only)
1708
- if (item.isActive) {
1709
- let entryX = pillXOff + item.pillWidth + 4;
1710
- for (const entry of item.entries) {
1711
- const entryG = gEl
1712
- .append('g')
1713
- .attr('data-legend-entry', entry.value.toLowerCase())
1714
- .style('cursor', 'pointer');
1715
-
1716
- entryG
1717
- .append('circle')
1718
- .attr('cx', entryX + LEGEND_DOT_R)
1719
- .attr('cy', LEGEND_HEIGHT / 2)
1720
- .attr('r', LEGEND_DOT_R)
1721
- .attr('fill', entry.color);
1722
-
1723
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
1724
- entryG
1725
- .append('text')
1726
- .attr('x', textX)
1727
- .attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
1728
- .attr('font-size', LEGEND_ENTRY_FONT_SIZE)
1729
- .attr('fill', palette.textMuted)
1730
- .text(entry.value);
1731
-
1732
- entryX =
1733
- textX +
1734
- measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
1735
- LEGEND_ENTRY_TRAIL;
1736
- }
1737
- }
1738
-
1739
- legendX += item.totalWidth + LEGEND_GROUP_GAP;
1740
- }
1602
+ const legendConfig: LegendConfig = {
1603
+ groups: resolvedGroups,
1604
+ position: { placement: 'top-center', titleRelation: 'below-title' },
1605
+ mode: 'fixed',
1606
+ };
1607
+ const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
1608
+ const legendG = svg
1609
+ .append('g')
1610
+ .attr('class', 'sequence-legend')
1611
+ .attr('transform', `translate(0,${legendY})`);
1612
+ renderLegendD3(
1613
+ legendG,
1614
+ legendConfig,
1615
+ legendState,
1616
+ palette,
1617
+ isDark,
1618
+ undefined,
1619
+ svgWidth
1620
+ );
1741
1621
  }
1742
1622
 
1743
1623
  // Render group boxes (behind participant shapes)
@@ -1882,6 +1762,7 @@ export function renderSequenceDiagram(
1882
1762
  y1: number;
1883
1763
  x2: number;
1884
1764
  y2: number;
1765
+ blockLine?: number;
1885
1766
  }> = [];
1886
1767
 
1887
1768
  // Recursive block renderer — draws borders/dividers now, defers label text
@@ -1890,12 +1771,17 @@ export function renderSequenceDiagram(
1890
1771
  if (!isSequenceBlock(el)) continue;
1891
1772
 
1892
1773
  const ifIndices = collectMsgIndices(el.children);
1893
- const elseIfBranchData: { label: string; indices: number[] }[] = [];
1774
+ const elseIfBranchData: {
1775
+ label: string;
1776
+ indices: number[];
1777
+ lineNumber: number;
1778
+ }[] = [];
1894
1779
  if (el.elseIfBranches) {
1895
1780
  for (const branch of el.elseIfBranches) {
1896
1781
  elseIfBranchData.push({
1897
1782
  label: branch.label,
1898
1783
  indices: collectMsgIndices(branch.children),
1784
+ lineNumber: branch.lineNumber,
1899
1785
  });
1900
1786
  }
1901
1787
  }
@@ -1985,6 +1871,7 @@ export function renderSequenceDiagram(
1985
1871
  y1: dividerY,
1986
1872
  x2: frameX + frameW,
1987
1873
  y2: dividerY,
1874
+ blockLine: branchData.lineNumber,
1988
1875
  });
1989
1876
  deferredLabels.push({
1990
1877
  x: frameX + 6,
@@ -1992,6 +1879,7 @@ export function renderSequenceDiagram(
1992
1879
  text: `else if ${branchData.label}`,
1993
1880
  bold: false,
1994
1881
  italic: true,
1882
+ blockLine: branchData.lineNumber,
1995
1883
  });
1996
1884
  }
1997
1885
  }
@@ -2012,6 +1900,7 @@ export function renderSequenceDiagram(
2012
1900
  y1: dividerY,
2013
1901
  x2: frameX + frameW,
2014
1902
  y2: dividerY,
1903
+ blockLine: el.elseLineNumber,
2015
1904
  });
2016
1905
  deferredLabels.push({
2017
1906
  x: frameX + 6,
@@ -2019,6 +1908,7 @@ export function renderSequenceDiagram(
2019
1908
  text: 'else',
2020
1909
  bold: false,
2021
1910
  italic: true,
1911
+ blockLine: el.elseLineNumber,
2022
1912
  });
2023
1913
  }
2024
1914
  }
@@ -2100,7 +1990,7 @@ export function renderSequenceDiagram(
2100
1990
 
2101
1991
  // Render deferred else dividers (on top of activations)
2102
1992
  for (const ln of deferredLines) {
2103
- svg
1993
+ const line = svg
2104
1994
  .append('line')
2105
1995
  .attr('x1', ln.x1)
2106
1996
  .attr('y1', ln.y1)
@@ -2108,7 +1998,10 @@ export function renderSequenceDiagram(
2108
1998
  .attr('y2', ln.y2)
2109
1999
  .attr('stroke', palette.textMuted)
2110
2000
  .attr('stroke-width', 1)
2111
- .attr('stroke-dasharray', '2 3');
2001
+ .attr('stroke-dasharray', '2 3')
2002
+ .attr('class', 'block-divider');
2003
+ if (ln.blockLine !== undefined)
2004
+ line.attr('data-block-line', String(ln.blockLine));
2112
2005
  }
2113
2006
 
2114
2007
  // Render deferred block labels (on top of activations)