@diagrammo/dgmo 0.2.8 → 0.2.10

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.
@@ -41,6 +41,8 @@ const NOTE_LINE_H = 14;
41
41
  const NOTE_GAP = 15;
42
42
  const NOTE_CHAR_W = 6;
43
43
  const NOTE_CHARS_PER_LINE = Math.floor((NOTE_MAX_W - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
44
+ const COLLAPSED_NOTE_H = 20;
45
+ const COLLAPSED_NOTE_W = 40;
44
46
 
45
47
  interface InlineSpan {
46
48
  text: string;
@@ -475,6 +477,7 @@ export interface SectionMessageGroup {
475
477
 
476
478
  export interface SequenceRenderOptions {
477
479
  collapsedSections?: Set<number>; // keyed by section lineNumber
480
+ expandedNoteLines?: Set<number>; // keyed by note lineNumber; undefined = all expanded (CLI default)
478
481
  exportWidth?: number; // Explicit width for CLI/export rendering (bypasses getBoundingClientRect)
479
482
  }
480
483
 
@@ -801,6 +804,12 @@ export function renderSequenceDiagram(
801
804
 
802
805
  const { title, messages, elements, groups, options: parsedOptions } = parsed;
803
806
  const collapsedSections = options?.collapsedSections;
807
+ const expandedNoteLines = options?.expandedNoteLines;
808
+ const collapseNotesDisabled = parsedOptions['collapse-notes']?.toLowerCase() === 'no';
809
+ // A note is expanded if: expandedNoteLines is undefined (CLI/export),
810
+ // collapse-notes: no is set, or the note's lineNumber is in the set.
811
+ const isNoteExpanded = (note: SequenceNote): boolean =>
812
+ expandedNoteLines === undefined || collapseNotesDisabled || expandedNoteLines.has(note.lineNumber);
804
813
  const participants = applyPositionOverrides(
805
814
  applyGroupOrdering(parsed.participants, groups)
806
815
  );
@@ -851,6 +860,24 @@ export function renderSequenceDiagram(
851
860
  msgToLastStep.set(step.messageIndex, si);
852
861
  });
853
862
 
863
+ // Map a note to the last render-step index of its preceding message
864
+ const findAssociatedLastStep = (note: SequenceNote): number => {
865
+ let bestMsgIndex = -1;
866
+ let bestLine = -1;
867
+ for (let mi = 0; mi < messages.length; mi++) {
868
+ if (
869
+ messages[mi].lineNumber < note.lineNumber &&
870
+ messages[mi].lineNumber > bestLine &&
871
+ !hiddenMsgIndices.has(mi)
872
+ ) {
873
+ bestLine = messages[mi].lineNumber;
874
+ bestMsgIndex = mi;
875
+ }
876
+ }
877
+ if (bestMsgIndex < 0) return -1;
878
+ return msgToLastStep.get(bestMsgIndex) ?? -1;
879
+ };
880
+
854
881
  // Find the first visible message index in an element subtree
855
882
  const findFirstMsgIndex = (els: SequenceElement[]): number => {
856
883
  for (const el of els) {
@@ -923,17 +950,41 @@ export function renderSequenceDiagram(
923
950
  const lines = wrapTextLines(text, NOTE_CHARS_PER_LINE);
924
951
  return lines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
925
952
  };
953
+ let trailingNoteSpace = 0; // extra space for notes at the end with no following message
926
954
  const markNoteSpacing = (els: SequenceElement[]): void => {
927
955
  for (let i = 0; i < els.length; i++) {
928
956
  const el = els[i];
929
957
  if (isSequenceNote(el)) {
930
- const noteH = computeNoteHeight(el.text);
931
- // Find the next non-note element after this note
932
- const nextIdx =
933
- i + 1 < els.length ? findFirstMsgIndex([els[i + 1]]) : -1;
934
- if (nextIdx >= 0) {
935
- addExtra(nextIdx, noteH + NOTE_OFFSET_BELOW);
958
+ // Accumulate heights of consecutive notes starting from this one
959
+ let accumulatedHeight = 0;
960
+ let j = i;
961
+ while (j < els.length && isSequenceNote(els[j])) {
962
+ const note = els[j] as SequenceNote;
963
+ const noteH = isNoteExpanded(note)
964
+ ? computeNoteHeight(note.text)
965
+ : COLLAPSED_NOTE_H;
966
+ accumulatedHeight += noteH + NOTE_OFFSET_BELOW;
967
+ j++;
968
+ }
969
+ // If notes are followed by a block, add extra space so the block frame
970
+ // (which extends above its first message) doesn't overlap with the note
971
+ if (j < els.length && isSequenceBlock(els[j])) {
972
+ accumulatedHeight += 20;
973
+ }
974
+ // Scan forward past sections, blocks, and other non-message elements to find next message
975
+ let nextMsgIdx = -1;
976
+ for (let k = j; k < els.length; k++) {
977
+ nextMsgIdx = findFirstMsgIndex([els[k]]);
978
+ if (nextMsgIdx >= 0) break;
936
979
  }
980
+ if (nextMsgIdx >= 0) {
981
+ addExtra(nextMsgIdx, accumulatedHeight);
982
+ } else {
983
+ // Notes at the end — track trailing space for viewport extension
984
+ trailingNoteSpace = Math.max(trailingNoteSpace, accumulatedHeight);
985
+ }
986
+ // Skip over the consecutive notes we just processed
987
+ i = j - 1;
937
988
  } else if (isSequenceBlock(el)) {
938
989
  markNoteSpacing(el.children);
939
990
  if (el.elseIfBranches) {
@@ -1123,16 +1174,66 @@ export function renderSequenceDiagram(
1123
1174
  sectionYPositions.set(sec.lineNumber, curY);
1124
1175
  curY += SECTION_BOTTOM_PAD;
1125
1176
  }
1177
+ // Extend for trailing notes that have no following message
1178
+ curY += trailingNoteSpace;
1126
1179
  layoutEndY = curY;
1127
1180
  }
1128
1181
 
1129
- const contentBottomY =
1182
+ // Helper: compute Y for a step index
1183
+ const stepY = (i: number) => stepYPositions[i];
1184
+
1185
+ // Compute absolute Y positions for each note element
1186
+ const noteYMap = new Map<SequenceNote, number>();
1187
+ {
1188
+ const computeNotePositions = (els: SequenceElement[]): void => {
1189
+ for (let i = 0; i < els.length; i++) {
1190
+ const el = els[i];
1191
+ if (isSequenceNote(el)) {
1192
+ const si = findAssociatedLastStep(el);
1193
+ if (si < 0) continue;
1194
+ // Check if there's a preceding note that we should stack below
1195
+ const prevNote = i > 0 && isSequenceNote(els[i - 1]) ? (els[i - 1] as SequenceNote) : null;
1196
+ const prevNoteY = prevNote ? noteYMap.get(prevNote) : undefined;
1197
+ let noteTopY: number;
1198
+ if (prevNoteY !== undefined && prevNote) {
1199
+ // Stack below previous note
1200
+ const prevNoteH = isNoteExpanded(prevNote)
1201
+ ? computeNoteHeight(prevNote.text)
1202
+ : COLLAPSED_NOTE_H;
1203
+ noteTopY = prevNoteY + prevNoteH + NOTE_OFFSET_BELOW;
1204
+ } else {
1205
+ // First note after a message
1206
+ noteTopY = stepY(si) + stepSpacing + NOTE_OFFSET_BELOW;
1207
+ }
1208
+ noteYMap.set(el, noteTopY);
1209
+ } else if (isSequenceBlock(el)) {
1210
+ computeNotePositions(el.children);
1211
+ if (el.elseIfBranches) {
1212
+ for (const branch of el.elseIfBranches) {
1213
+ computeNotePositions(branch.children);
1214
+ }
1215
+ }
1216
+ computeNotePositions(el.elseChildren);
1217
+ }
1218
+ }
1219
+ };
1220
+ if (elements && elements.length > 0) {
1221
+ computeNotePositions(elements);
1222
+ }
1223
+ }
1224
+
1225
+ // Ensure contentBottomY accounts for all note extents
1226
+ let contentBottomY =
1130
1227
  renderSteps.length > 0
1131
1228
  ? Math.max(
1132
1229
  stepYPositions[stepYPositions.length - 1] + stepSpacing,
1133
1230
  layoutEndY
1134
1231
  )
1135
1232
  : layoutEndY;
1233
+ for (const [note, noteTopY] of noteYMap) {
1234
+ const noteH = isNoteExpanded(note) ? computeNoteHeight(note.text) : COLLAPSED_NOTE_H;
1235
+ contentBottomY = Math.max(contentBottomY, noteTopY + noteH + NOTE_OFFSET_BELOW);
1236
+ }
1136
1237
  const messageAreaHeight = contentBottomY - lifelineStartY0;
1137
1238
  const lifelineLength = messageAreaHeight + LIFELINE_TAIL;
1138
1239
  const totalWidth = Math.max(
@@ -1229,8 +1330,9 @@ export function renderSequenceDiagram(
1229
1330
 
1230
1331
  // Render title
1231
1332
  if (title) {
1232
- svg
1333
+ const titleEl = svg
1233
1334
  .append('text')
1335
+ .attr('class', 'chart-title')
1234
1336
  .attr('x', svgWidth / 2)
1235
1337
  .attr('y', 30)
1236
1338
  .attr('text-anchor', 'middle')
@@ -1238,6 +1340,10 @@ export function renderSequenceDiagram(
1238
1340
  .attr('font-size', 20)
1239
1341
  .attr('font-weight', 'bold')
1240
1342
  .text(title);
1343
+
1344
+ if (parsed.titleLineNumber) {
1345
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
1346
+ }
1241
1347
  }
1242
1348
 
1243
1349
  // Render group boxes (behind participant shapes)
@@ -1318,9 +1424,6 @@ export function renderSequenceDiagram(
1318
1424
  .attr('class', 'lifeline');
1319
1425
  });
1320
1426
 
1321
- // Helper: compute Y for a step index
1322
- const stepY = (i: number) => stepYPositions[i];
1323
-
1324
1427
  // Render block frames (behind everything else)
1325
1428
  const FRAME_PADDING_X = 30;
1326
1429
  const FRAME_PADDING_TOP = 42;
@@ -1862,136 +1965,183 @@ export function renderSequenceDiagram(
1862
1965
  ? mix(palette.surface, palette.bg, 50)
1863
1966
  : mix(palette.bg, palette.surface, 15);
1864
1967
 
1865
- const findAssociatedStep = (note: SequenceNote): number => {
1866
- let bestMsgIndex = -1;
1867
- let bestLine = -1;
1868
- for (let mi = 0; mi < messages.length; mi++) {
1869
- if (
1870
- messages[mi].lineNumber < note.lineNumber &&
1871
- messages[mi].lineNumber > bestLine &&
1872
- !hiddenMsgIndices.has(mi)
1873
- ) {
1874
- bestLine = messages[mi].lineNumber;
1875
- bestMsgIndex = mi;
1876
- }
1877
- }
1878
- if (bestMsgIndex < 0) return -1;
1879
- return msgToFirstStep.get(bestMsgIndex) ?? -1;
1880
- };
1968
+ const collapsedNoteFill = mix(palette.textMuted, palette.bg, 15);
1881
1969
 
1882
1970
  const renderNoteElements = (els: SequenceElement[]): void => {
1883
1971
  for (const el of els) {
1884
1972
  if (isSequenceNote(el)) {
1885
1973
  const px = participantX.get(el.participantId);
1886
1974
  if (px === undefined) continue;
1887
- const si = findAssociatedStep(el);
1888
- if (si < 0) continue;
1889
- const noteY = stepY(si);
1890
-
1891
- const wrappedLines = wrapTextLines(el.text, NOTE_CHARS_PER_LINE);
1892
- const noteH = wrappedLines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
1893
- const maxLineLen = Math.max(...wrappedLines.map((l) => l.length));
1894
- const noteW = Math.min(
1895
- NOTE_MAX_W,
1896
- Math.max(80, maxLineLen * NOTE_CHAR_W + NOTE_PAD_H * 2 + NOTE_FOLD)
1897
- );
1898
- const isRight = el.position === 'right';
1899
- const noteX = isRight
1900
- ? px + ACTIVATION_WIDTH + NOTE_GAP
1901
- : px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
1902
- const noteTopY = noteY + NOTE_OFFSET_BELOW;
1903
-
1904
- // Wrap in <g> with data attributes for toggle support
1905
- const noteG = svg
1906
- .append('g')
1907
- .attr('class', 'note')
1908
- .attr('data-note-toggle', '')
1909
- .attr('data-line-number', String(el.lineNumber))
1910
- .attr('data-line-end', String(el.endLineNumber));
1911
-
1912
- // Folded-corner path
1913
- noteG
1914
- .append('path')
1915
- .attr(
1916
- 'd',
1917
- [
1918
- `M ${noteX} ${noteTopY}`,
1919
- `L ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
1920
- `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
1921
- `L ${noteX + noteW} ${noteTopY + noteH}`,
1922
- `L ${noteX} ${noteTopY + noteH}`,
1923
- 'Z',
1924
- ].join(' ')
1925
- )
1926
- .attr('fill', noteFill)
1927
- .attr('stroke', palette.textMuted)
1928
- .attr('stroke-width', 0.75)
1929
- .attr('class', 'note-box');
1975
+ const noteTopY = noteYMap.get(el);
1976
+ if (noteTopY === undefined) continue;
1930
1977
 
1931
- // Fold triangle
1932
- noteG
1933
- .append('path')
1934
- .attr(
1935
- 'd',
1936
- [
1937
- `M ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
1938
- `L ${noteX + noteW - NOTE_FOLD} ${noteTopY + NOTE_FOLD}`,
1939
- `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
1940
- ].join(' ')
1941
- )
1942
- .attr('fill', 'none')
1943
- .attr('stroke', palette.textMuted)
1944
- .attr('stroke-width', 0.75)
1945
- .attr('class', 'note-fold');
1946
-
1947
- // Render text with inline markdown
1948
- wrappedLines.forEach((line, li) => {
1949
- const textY =
1950
- noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
1951
- const isBullet = line.startsWith('- ');
1952
- const bulletIndent = isBullet ? 10 : 0;
1953
- const displayLine = isBullet ? line.slice(2) : line;
1954
- const textEl = noteG
1955
- .append('text')
1956
- .attr('x', noteX + NOTE_PAD_H + bulletIndent)
1957
- .attr('y', textY)
1958
- .attr('fill', palette.text)
1959
- .attr('font-size', NOTE_FONT_SIZE)
1960
- .attr('class', 'note-text');
1978
+ const expanded = isNoteExpanded(el);
1979
+ const isRight = el.position === 'right';
1961
1980
 
1962
- if (isBullet) {
1963
- noteG
1981
+ if (expanded) {
1982
+ // --- Expanded note: full folded-corner box with wrapped text ---
1983
+ const wrappedLines = wrapTextLines(el.text, NOTE_CHARS_PER_LINE);
1984
+ const noteH = wrappedLines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
1985
+ const maxLineLen = Math.max(...wrappedLines.map((l) => l.length));
1986
+ const noteW = Math.min(
1987
+ NOTE_MAX_W,
1988
+ Math.max(80, maxLineLen * NOTE_CHAR_W + NOTE_PAD_H * 2 + NOTE_FOLD)
1989
+ );
1990
+ const noteX = isRight
1991
+ ? px + ACTIVATION_WIDTH + NOTE_GAP
1992
+ : px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
1993
+
1994
+ const noteG = svg
1995
+ .append('g')
1996
+ .attr('class', 'note')
1997
+ .attr('data-note-toggle', '')
1998
+ .attr('data-line-number', String(el.lineNumber))
1999
+ .attr('data-line-end', String(el.endLineNumber));
2000
+
2001
+ // Folded-corner path
2002
+ noteG
2003
+ .append('path')
2004
+ .attr(
2005
+ 'd',
2006
+ [
2007
+ `M ${noteX} ${noteTopY}`,
2008
+ `L ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
2009
+ `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
2010
+ `L ${noteX + noteW} ${noteTopY + noteH}`,
2011
+ `L ${noteX} ${noteTopY + noteH}`,
2012
+ 'Z',
2013
+ ].join(' ')
2014
+ )
2015
+ .attr('fill', noteFill)
2016
+ .attr('stroke', palette.textMuted)
2017
+ .attr('stroke-width', 0.75)
2018
+ .attr('class', 'note-box');
2019
+
2020
+ // Fold triangle
2021
+ noteG
2022
+ .append('path')
2023
+ .attr(
2024
+ 'd',
2025
+ [
2026
+ `M ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
2027
+ `L ${noteX + noteW - NOTE_FOLD} ${noteTopY + NOTE_FOLD}`,
2028
+ `L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
2029
+ ].join(' ')
2030
+ )
2031
+ .attr('fill', 'none')
2032
+ .attr('stroke', palette.textMuted)
2033
+ .attr('stroke-width', 0.75)
2034
+ .attr('class', 'note-fold');
2035
+
2036
+ // Render text with inline markdown
2037
+ wrappedLines.forEach((line, li) => {
2038
+ const textY =
2039
+ noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
2040
+ const isBullet = line.startsWith('- ');
2041
+ const bulletIndent = isBullet ? 10 : 0;
2042
+ const displayLine = isBullet ? line.slice(2) : line;
2043
+ const textEl = noteG
1964
2044
  .append('text')
1965
- .attr('x', noteX + NOTE_PAD_H)
2045
+ .attr('x', noteX + NOTE_PAD_H + bulletIndent)
1966
2046
  .attr('y', textY)
1967
2047
  .attr('fill', palette.text)
1968
2048
  .attr('font-size', NOTE_FONT_SIZE)
1969
- .text('\u2022');
1970
- }
2049
+ .attr('class', 'note-text');
2050
+
2051
+ if (isBullet) {
2052
+ noteG
2053
+ .append('text')
2054
+ .attr('x', noteX + NOTE_PAD_H)
2055
+ .attr('y', textY)
2056
+ .attr('fill', palette.text)
2057
+ .attr('font-size', NOTE_FONT_SIZE)
2058
+ .text('\u2022');
2059
+ }
1971
2060
 
1972
- const spans = parseInlineMarkdown(displayLine);
1973
- for (const span of spans) {
1974
- if (span.href) {
1975
- const a = textEl
1976
- .append('a')
1977
- .attr('href', span.href);
1978
- a.append('tspan')
1979
- .text(span.text)
1980
- .attr('fill', palette.primary)
1981
- .style('text-decoration', 'underline');
1982
- } else {
1983
- const tspan = textEl
1984
- .append('tspan')
1985
- .text(span.text);
1986
- if (span.bold) tspan.attr('font-weight', 'bold');
1987
- if (span.italic) tspan.attr('font-style', 'italic');
1988
- if (span.code)
1989
- tspan
1990
- .attr('font-family', 'monospace')
1991
- .attr('font-size', NOTE_FONT_SIZE - 1);
2061
+ const spans = parseInlineMarkdown(displayLine);
2062
+ for (const span of spans) {
2063
+ if (span.href) {
2064
+ const a = textEl
2065
+ .append('a')
2066
+ .attr('href', span.href);
2067
+ a.append('tspan')
2068
+ .text(span.text)
2069
+ .attr('fill', palette.primary)
2070
+ .style('text-decoration', 'underline');
2071
+ } else {
2072
+ const tspan = textEl
2073
+ .append('tspan')
2074
+ .text(span.text);
2075
+ if (span.bold) tspan.attr('font-weight', 'bold');
2076
+ if (span.italic) tspan.attr('font-style', 'italic');
2077
+ if (span.code)
2078
+ tspan
2079
+ .attr('font-family', 'monospace')
2080
+ .attr('font-size', NOTE_FONT_SIZE - 1);
2081
+ }
1992
2082
  }
1993
- }
1994
- });
2083
+ });
2084
+ } else {
2085
+ // --- Collapsed note: compact indicator ---
2086
+ const cFold = 6;
2087
+ const noteX = isRight
2088
+ ? px + ACTIVATION_WIDTH + NOTE_GAP
2089
+ : px - ACTIVATION_WIDTH - NOTE_GAP - COLLAPSED_NOTE_W;
2090
+
2091
+ const noteG = svg
2092
+ .append('g')
2093
+ .attr('class', 'note note-collapsed')
2094
+ .attr('data-note-toggle', '')
2095
+ .attr('data-line-number', String(el.lineNumber))
2096
+ .attr('data-line-end', String(el.endLineNumber))
2097
+ .style('cursor', 'pointer');
2098
+
2099
+ // Small folded-corner rectangle
2100
+ noteG
2101
+ .append('path')
2102
+ .attr(
2103
+ 'd',
2104
+ [
2105
+ `M ${noteX} ${noteTopY}`,
2106
+ `L ${noteX + COLLAPSED_NOTE_W - cFold} ${noteTopY}`,
2107
+ `L ${noteX + COLLAPSED_NOTE_W} ${noteTopY + cFold}`,
2108
+ `L ${noteX + COLLAPSED_NOTE_W} ${noteTopY + COLLAPSED_NOTE_H}`,
2109
+ `L ${noteX} ${noteTopY + COLLAPSED_NOTE_H}`,
2110
+ 'Z',
2111
+ ].join(' ')
2112
+ )
2113
+ .attr('fill', collapsedNoteFill)
2114
+ .attr('stroke', palette.border)
2115
+ .attr('stroke-width', 0.75)
2116
+ .attr('class', 'note-box');
2117
+
2118
+ // Fold triangle
2119
+ noteG
2120
+ .append('path')
2121
+ .attr(
2122
+ 'd',
2123
+ [
2124
+ `M ${noteX + COLLAPSED_NOTE_W - cFold} ${noteTopY}`,
2125
+ `L ${noteX + COLLAPSED_NOTE_W - cFold} ${noteTopY + cFold}`,
2126
+ `L ${noteX + COLLAPSED_NOTE_W} ${noteTopY + cFold}`,
2127
+ ].join(' ')
2128
+ )
2129
+ .attr('fill', 'none')
2130
+ .attr('stroke', palette.border)
2131
+ .attr('stroke-width', 0.75)
2132
+ .attr('class', 'note-fold');
2133
+
2134
+ // "..." text
2135
+ noteG
2136
+ .append('text')
2137
+ .attr('x', noteX + COLLAPSED_NOTE_W / 2)
2138
+ .attr('y', noteTopY + COLLAPSED_NOTE_H / 2 + 3)
2139
+ .attr('text-anchor', 'middle')
2140
+ .attr('fill', palette.textMuted)
2141
+ .attr('font-size', 9)
2142
+ .attr('class', 'note-text')
2143
+ .text('\u2026');
2144
+ }
1995
2145
  } else if (isSequenceBlock(el)) {
1996
2146
  renderNoteElements(el.children);
1997
2147
  if (el.elseIfBranches) {
@@ -2009,6 +2159,40 @@ export function renderSequenceDiagram(
2009
2159
  }
2010
2160
  }
2011
2161
 
2162
+ /**
2163
+ * Build a mapping from each note's lineNumber to the lineNumber of its
2164
+ * associated message (the last message before the note in document order).
2165
+ * Used by the app to expand notes when cursor is on the associated message.
2166
+ */
2167
+ export function buildNoteMessageMap(elements: SequenceElement[]): Map<number, number> {
2168
+ const map = new Map<number, number>();
2169
+ let lastMessageLine = -1;
2170
+
2171
+ const walk = (els: SequenceElement[]): void => {
2172
+ for (const el of els) {
2173
+ if (isSequenceNote(el)) {
2174
+ if (lastMessageLine >= 0) {
2175
+ map.set(el.lineNumber, lastMessageLine);
2176
+ }
2177
+ } else if (isSequenceBlock(el)) {
2178
+ walk(el.children);
2179
+ if (el.elseIfBranches) {
2180
+ for (const branch of el.elseIfBranches) {
2181
+ walk(branch.children);
2182
+ }
2183
+ }
2184
+ walk(el.elseChildren);
2185
+ } else if (!isSequenceSection(el)) {
2186
+ // It's a message
2187
+ const msg = el as SequenceMessage;
2188
+ lastMessageLine = msg.lineNumber;
2189
+ }
2190
+ }
2191
+ };
2192
+ walk(elements);
2193
+ return map;
2194
+ }
2195
+
2012
2196
  function renderParticipant(
2013
2197
  svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
2014
2198
  participant: SequenceParticipant,