@diagrammo/dgmo 0.2.6 → 0.2.7

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.
@@ -11,9 +11,10 @@ import type {
11
11
  SequenceElement,
12
12
  SequenceGroup,
13
13
  SequenceMessage,
14
+ SequenceNote,
14
15
  SequenceParticipant,
15
16
  } from './parser';
16
- import { isSequenceBlock, isSequenceSection } from './parser';
17
+ import { isSequenceBlock, isSequenceSection, isSequenceNote } from './parser';
17
18
 
18
19
  // ============================================================
19
20
  // Layout Constants
@@ -30,6 +31,62 @@ const MESSAGE_START_OFFSET = 30;
30
31
  const LIFELINE_TAIL = 30;
31
32
  const ARROWHEAD_SIZE = 8;
32
33
 
34
+ // Note rendering constants
35
+ const NOTE_MAX_W = 200;
36
+ const NOTE_FOLD = 10;
37
+ const NOTE_PAD_H = 8;
38
+ const NOTE_PAD_V = 6;
39
+ const NOTE_FONT_SIZE = 10;
40
+ const NOTE_LINE_H = 14;
41
+ const NOTE_GAP = 15;
42
+ const NOTE_CHAR_W = 6;
43
+ const NOTE_CHARS_PER_LINE = Math.floor((NOTE_MAX_W - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
44
+
45
+ interface InlineSpan {
46
+ text: string;
47
+ bold?: boolean;
48
+ italic?: boolean;
49
+ code?: boolean;
50
+ href?: string;
51
+ }
52
+
53
+ function parseInlineMarkdown(text: string): InlineSpan[] {
54
+ const spans: InlineSpan[] = [];
55
+ const regex = /\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`|\[(.+?)\]\((.+?)\)|([^*`[]+)/g;
56
+ let match;
57
+ while ((match = regex.exec(text)) !== null) {
58
+ if (match[1]) spans.push({ text: match[1], bold: true });
59
+ else if (match[2]) spans.push({ text: match[2], italic: true });
60
+ else if (match[3]) spans.push({ text: match[3], code: true });
61
+ else if (match[4]) spans.push({ text: match[4], href: match[5] });
62
+ else if (match[6]) spans.push({ text: match[6] });
63
+ }
64
+ return spans;
65
+ }
66
+
67
+ function wrapTextLines(text: string, maxChars: number): string[] {
68
+ const rawLines = text.split('\n');
69
+ const wrapped: string[] = [];
70
+ for (const line of rawLines) {
71
+ if (line.length <= maxChars) {
72
+ wrapped.push(line);
73
+ } else {
74
+ const words = line.split(' ');
75
+ let current = '';
76
+ for (const word of words) {
77
+ if (current && (current + ' ' + word).length > maxChars) {
78
+ wrapped.push(current);
79
+ current = word;
80
+ } else {
81
+ current = current ? current + ' ' + word : word;
82
+ }
83
+ }
84
+ if (current) wrapped.push(current);
85
+ }
86
+ }
87
+ return wrapped;
88
+ }
89
+
33
90
  // Mix two hex colors in sRGB: pct% of a, rest of b
34
91
  function mix(a: string, b: string, pct: number): string {
35
92
  const parse = (h: string) => {
@@ -440,11 +497,16 @@ export function groupMessagesBySection(
440
497
  ...collectIndices(el.children),
441
498
  ...collectIndices(el.elseChildren)
442
499
  );
443
- } else if (isSequenceSection(el)) {
444
- // Sections inside blocks are not top-level — skip
500
+ if (el.elseIfBranches) {
501
+ for (const branch of el.elseIfBranches) {
502
+ indices.push(...collectIndices(branch.children));
503
+ }
504
+ }
505
+ } else if (isSequenceSection(el) || isSequenceNote(el)) {
506
+ // Sections and notes inside blocks are not messages — skip
445
507
  continue;
446
508
  } else {
447
- const idx = messages.indexOf(el);
509
+ const idx = messages.indexOf(el as SequenceMessage);
448
510
  if (idx >= 0) indices.push(idx);
449
511
  }
450
512
  }
@@ -460,8 +522,8 @@ export function groupMessagesBySection(
460
522
  // Collect messages from this element into the current group
461
523
  if (isSequenceBlock(el)) {
462
524
  currentGroup.messageIndices.push(...collectIndices([el]));
463
- } else {
464
- const idx = messages.indexOf(el);
525
+ } else if (!isSequenceNote(el)) {
526
+ const idx = messages.indexOf(el as SequenceMessage);
465
527
  if (idx >= 0) currentGroup.messageIndices.push(idx);
466
528
  }
467
529
  }
@@ -793,8 +855,16 @@ export function renderSequenceDiagram(
793
855
  if (isSequenceBlock(el)) {
794
856
  const idx = findFirstMsgIndex(el.children);
795
857
  if (idx >= 0) return idx;
796
- } else if (!isSequenceSection(el)) {
797
- const idx = messages.indexOf(el);
858
+ if (el.elseIfBranches) {
859
+ for (const branch of el.elseIfBranches) {
860
+ const branchIdx = findFirstMsgIndex(branch.children);
861
+ if (branchIdx >= 0) return branchIdx;
862
+ }
863
+ }
864
+ const elseIdx = findFirstMsgIndex(el.elseChildren);
865
+ if (elseIdx >= 0) return elseIdx;
866
+ } else if (!isSequenceSection(el) && !isSequenceNote(el)) {
867
+ const idx = messages.indexOf(el as SequenceMessage);
798
868
  if (idx >= 0 && !hiddenMsgIndices.has(idx)) return idx;
799
869
  }
800
870
  }
@@ -820,6 +890,14 @@ export function renderSequenceDiagram(
820
890
  const firstIdx = findFirstMsgIndex(el.children);
821
891
  if (firstIdx >= 0) addExtra(firstIdx, BLOCK_HEADER_SPACE);
822
892
 
893
+ if (el.elseIfBranches) {
894
+ for (const branch of el.elseIfBranches) {
895
+ const firstBranchIdx = findFirstMsgIndex(branch.children);
896
+ if (firstBranchIdx >= 0) addExtra(firstBranchIdx, BLOCK_HEADER_SPACE);
897
+ markBlockSpacing(branch.children);
898
+ }
899
+ }
900
+
823
901
  const firstElseIdx = findFirstMsgIndex(el.elseChildren);
824
902
  if (firstElseIdx >= 0) addExtra(firstElseIdx, BLOCK_HEADER_SPACE);
825
903
 
@@ -856,16 +934,28 @@ export function renderSequenceDiagram(
856
934
  for (const child of block.children) {
857
935
  if (isSequenceBlock(child)) {
858
936
  indices.push(...collectMsgIndicesFromBlock(child));
859
- } else if (!isSequenceSection(child)) {
860
- const idx = messages.indexOf(child);
937
+ } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
938
+ const idx = messages.indexOf(child as SequenceMessage);
861
939
  if (idx >= 0) indices.push(idx);
862
940
  }
863
941
  }
942
+ if (block.elseIfBranches) {
943
+ for (const branch of block.elseIfBranches) {
944
+ for (const child of branch.children) {
945
+ if (isSequenceBlock(child)) {
946
+ indices.push(...collectMsgIndicesFromBlock(child));
947
+ } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
948
+ const idx = messages.indexOf(child as SequenceMessage);
949
+ if (idx >= 0) indices.push(idx);
950
+ }
951
+ }
952
+ }
953
+ }
864
954
  for (const child of block.elseChildren) {
865
955
  if (isSequenceBlock(child)) {
866
956
  indices.push(...collectMsgIndicesFromBlock(child));
867
- } else if (!isSequenceSection(child)) {
868
- const idx = messages.indexOf(child);
957
+ } else if (!isSequenceSection(child) && !isSequenceNote(child)) {
958
+ const idx = messages.indexOf(child as SequenceMessage);
869
959
  if (idx >= 0) indices.push(idx);
870
960
  }
871
961
  }
@@ -881,7 +971,7 @@ export function renderSequenceDiagram(
881
971
  } else if (isSequenceBlock(el)) {
882
972
  currentTarget.push(...collectMsgIndicesFromBlock(el));
883
973
  } else {
884
- const idx = messages.indexOf(el);
974
+ const idx = messages.indexOf(el as SequenceMessage);
885
975
  if (idx >= 0) currentTarget.push(idx);
886
976
  }
887
977
  }
@@ -1212,8 +1302,13 @@ export function renderSequenceDiagram(
1212
1302
  ...collectMsgIndices(el.children),
1213
1303
  ...collectMsgIndices(el.elseChildren)
1214
1304
  );
1215
- } else if (!isSequenceSection(el)) {
1216
- const idx = messages.indexOf(el);
1305
+ if (el.elseIfBranches) {
1306
+ for (const branch of el.elseIfBranches) {
1307
+ indices.push(...collectMsgIndices(branch.children));
1308
+ }
1309
+ }
1310
+ } else if (!isSequenceSection(el) && !isSequenceNote(el)) {
1311
+ const idx = messages.indexOf(el as SequenceMessage);
1217
1312
  if (idx >= 0) indices.push(idx);
1218
1313
  }
1219
1314
  }
@@ -1242,8 +1337,21 @@ export function renderSequenceDiagram(
1242
1337
  if (!isSequenceBlock(el)) continue;
1243
1338
 
1244
1339
  const ifIndices = collectMsgIndices(el.children);
1340
+ const elseIfBranchData: { label: string; indices: number[] }[] = [];
1341
+ if (el.elseIfBranches) {
1342
+ for (const branch of el.elseIfBranches) {
1343
+ elseIfBranchData.push({
1344
+ label: branch.label,
1345
+ indices: collectMsgIndices(branch.children),
1346
+ });
1347
+ }
1348
+ }
1245
1349
  const elseIndices = collectMsgIndices(el.elseChildren);
1246
- const allIndices = [...ifIndices, ...elseIndices];
1350
+ const allIndices = [
1351
+ ...ifIndices,
1352
+ ...elseIfBranchData.flatMap((b) => b.indices),
1353
+ ...elseIndices,
1354
+ ];
1247
1355
  if (allIndices.length === 0) continue;
1248
1356
 
1249
1357
  // Find render step range
@@ -1308,6 +1416,34 @@ export function renderSequenceDiagram(
1308
1416
  blockLine: el.lineNumber,
1309
1417
  });
1310
1418
 
1419
+ // Else-if dividers
1420
+ for (const branchData of elseIfBranchData) {
1421
+ if (branchData.indices.length > 0) {
1422
+ let firstBranchStep = Infinity;
1423
+ for (const mi of branchData.indices) {
1424
+ const first = msgToFirstStep.get(mi);
1425
+ if (first !== undefined)
1426
+ firstBranchStep = Math.min(firstBranchStep, first);
1427
+ }
1428
+ if (firstBranchStep < Infinity) {
1429
+ const dividerY = stepY(firstBranchStep) - stepSpacing / 2;
1430
+ deferredLines.push({
1431
+ x1: frameX,
1432
+ y1: dividerY,
1433
+ x2: frameX + frameW,
1434
+ y2: dividerY,
1435
+ });
1436
+ deferredLabels.push({
1437
+ x: frameX + 6,
1438
+ y: dividerY + 14,
1439
+ text: `else if ${branchData.label}`,
1440
+ bold: false,
1441
+ italic: true,
1442
+ });
1443
+ }
1444
+ }
1445
+ }
1446
+
1311
1447
  // Else divider
1312
1448
  if (elseIndices.length > 0) {
1313
1449
  let firstElseStep = Infinity;
@@ -1336,6 +1472,11 @@ export function renderSequenceDiagram(
1336
1472
 
1337
1473
  // Recurse into nested blocks
1338
1474
  renderBlockFrames(el.children, depth + 1);
1475
+ if (el.elseIfBranches) {
1476
+ for (const branch of el.elseIfBranches) {
1477
+ renderBlockFrames(branch.children, depth + 1);
1478
+ }
1479
+ }
1339
1480
  renderBlockFrames(el.elseChildren, depth + 1);
1340
1481
  }
1341
1482
  };
@@ -1681,6 +1822,159 @@ export function renderSequenceDiagram(
1681
1822
  }
1682
1823
  }
1683
1824
  });
1825
+
1826
+ // Render notes — folded-corner boxes attached to participant lifelines
1827
+ const noteFill = isDark
1828
+ ? mix(palette.surface, palette.bg, 50)
1829
+ : mix(palette.bg, palette.surface, 15);
1830
+
1831
+ const findAssociatedStep = (note: SequenceNote): number => {
1832
+ let bestMsgIndex = -1;
1833
+ let bestLine = -1;
1834
+ for (let mi = 0; mi < messages.length; mi++) {
1835
+ if (
1836
+ messages[mi].lineNumber < note.lineNumber &&
1837
+ messages[mi].lineNumber > bestLine &&
1838
+ !hiddenMsgIndices.has(mi)
1839
+ ) {
1840
+ bestLine = messages[mi].lineNumber;
1841
+ bestMsgIndex = mi;
1842
+ }
1843
+ }
1844
+ if (bestMsgIndex < 0) return -1;
1845
+ return msgToFirstStep.get(bestMsgIndex) ?? -1;
1846
+ };
1847
+
1848
+ const renderNoteElements = (els: SequenceElement[]): void => {
1849
+ for (const el of els) {
1850
+ if (isSequenceNote(el)) {
1851
+ const px = participantX.get(el.participantId);
1852
+ if (px === undefined) continue;
1853
+ const si = findAssociatedStep(el);
1854
+ if (si < 0) continue;
1855
+ const noteY = stepY(si);
1856
+
1857
+ const wrappedLines = wrapTextLines(el.text, NOTE_CHARS_PER_LINE);
1858
+ const noteH = wrappedLines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
1859
+ const maxLineLen = Math.max(...wrappedLines.map((l) => l.length));
1860
+ const noteW = Math.min(
1861
+ NOTE_MAX_W,
1862
+ Math.max(80, maxLineLen * NOTE_CHAR_W + NOTE_PAD_H * 2 + NOTE_FOLD)
1863
+ );
1864
+ const isRight = el.position === 'right';
1865
+ const noteX = isRight
1866
+ ? px + ACTIVATION_WIDTH + NOTE_GAP
1867
+ : px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
1868
+ const noteTopY = noteY - noteH / 2;
1869
+
1870
+ // Wrap in <g> with data attributes for toggle support
1871
+ const noteG = svg
1872
+ .append('g')
1873
+ .attr('class', 'note')
1874
+ .attr('data-note-toggle', '')
1875
+ .attr('data-line-number', String(el.lineNumber));
1876
+
1877
+ // Folded-corner path
1878
+ noteG
1879
+ .append('path')
1880
+ .attr(
1881
+ 'd',
1882
+ [
1883
+ `M ${noteX} ${noteTopY}`,
1884
+ `L ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
1885
+ `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
1886
+ `L ${noteX + noteW} ${noteTopY + noteH}`,
1887
+ `L ${noteX} ${noteTopY + noteH}`,
1888
+ 'Z',
1889
+ ].join(' ')
1890
+ )
1891
+ .attr('fill', noteFill)
1892
+ .attr('stroke', palette.textMuted)
1893
+ .attr('stroke-width', 0.75)
1894
+ .attr('class', 'note-box');
1895
+
1896
+ // Fold triangle
1897
+ noteG
1898
+ .append('path')
1899
+ .attr(
1900
+ 'd',
1901
+ [
1902
+ `M ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
1903
+ `L ${noteX + noteW - NOTE_FOLD} ${noteTopY + NOTE_FOLD}`,
1904
+ `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
1905
+ ].join(' ')
1906
+ )
1907
+ .attr('fill', 'none')
1908
+ .attr('stroke', palette.textMuted)
1909
+ .attr('stroke-width', 0.75)
1910
+ .attr('class', 'note-fold');
1911
+
1912
+ // Dashed connector to lifeline
1913
+ const connectorNoteX = isRight ? noteX : noteX + noteW;
1914
+ const connectorLifeX = isRight
1915
+ ? px + ACTIVATION_WIDTH / 2
1916
+ : px - ACTIVATION_WIDTH / 2;
1917
+ noteG
1918
+ .append('line')
1919
+ .attr('x1', connectorNoteX)
1920
+ .attr('y1', noteY)
1921
+ .attr('x2', connectorLifeX)
1922
+ .attr('y2', noteY)
1923
+ .attr('stroke', palette.textMuted)
1924
+ .attr('stroke-width', 0.75)
1925
+ .attr('stroke-dasharray', '3 2')
1926
+ .attr('class', 'note-connector');
1927
+
1928
+ // Render text with inline markdown
1929
+ wrappedLines.forEach((line, li) => {
1930
+ const textY =
1931
+ noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
1932
+ const textEl = noteG
1933
+ .append('text')
1934
+ .attr('x', noteX + NOTE_PAD_H)
1935
+ .attr('y', textY)
1936
+ .attr('fill', palette.text)
1937
+ .attr('font-size', NOTE_FONT_SIZE)
1938
+ .attr('class', 'note-text');
1939
+
1940
+ const spans = parseInlineMarkdown(line);
1941
+ for (const span of spans) {
1942
+ if (span.href) {
1943
+ const a = textEl
1944
+ .append('a')
1945
+ .attr('href', span.href);
1946
+ a.append('tspan')
1947
+ .text(span.text)
1948
+ .attr('fill', palette.primary)
1949
+ .style('text-decoration', 'underline');
1950
+ } else {
1951
+ const tspan = textEl
1952
+ .append('tspan')
1953
+ .text(span.text);
1954
+ if (span.bold) tspan.attr('font-weight', 'bold');
1955
+ if (span.italic) tspan.attr('font-style', 'italic');
1956
+ if (span.code)
1957
+ tspan
1958
+ .attr('font-family', 'monospace')
1959
+ .attr('font-size', NOTE_FONT_SIZE - 1);
1960
+ }
1961
+ }
1962
+ });
1963
+ } else if (isSequenceBlock(el)) {
1964
+ renderNoteElements(el.children);
1965
+ if (el.elseIfBranches) {
1966
+ for (const branch of el.elseIfBranches) {
1967
+ renderNoteElements(branch.children);
1968
+ }
1969
+ }
1970
+ renderNoteElements(el.elseChildren);
1971
+ }
1972
+ }
1973
+ };
1974
+
1975
+ if (elements && elements.length > 0) {
1976
+ renderNoteElements(elements);
1977
+ }
1684
1978
  }
1685
1979
 
1686
1980
  function renderParticipant(