@diagrammo/dgmo 0.2.9 → 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.
- package/README.md +29 -1
- package/dist/cli.cjs +105 -105
- package/dist/index.cjs +7484 -7270
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +7439 -7229
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chart.ts +9 -0
- package/src/cli.ts +14 -34
- package/src/d3.ts +42 -6
- package/src/echarts.ts +54 -2
- package/src/index.ts +7 -0
- package/src/render.ts +68 -0
- package/src/sequence/renderer.ts +305 -126
package/src/sequence/renderer.ts
CHANGED
|
@@ -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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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;
|
|
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);
|
|
936
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
|
-
|
|
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(
|
|
@@ -1323,9 +1424,6 @@ export function renderSequenceDiagram(
|
|
|
1323
1424
|
.attr('class', 'lifeline');
|
|
1324
1425
|
});
|
|
1325
1426
|
|
|
1326
|
-
// Helper: compute Y for a step index
|
|
1327
|
-
const stepY = (i: number) => stepYPositions[i];
|
|
1328
|
-
|
|
1329
1427
|
// Render block frames (behind everything else)
|
|
1330
1428
|
const FRAME_PADDING_X = 30;
|
|
1331
1429
|
const FRAME_PADDING_TOP = 42;
|
|
@@ -1867,136 +1965,183 @@ export function renderSequenceDiagram(
|
|
|
1867
1965
|
? mix(palette.surface, palette.bg, 50)
|
|
1868
1966
|
: mix(palette.bg, palette.surface, 15);
|
|
1869
1967
|
|
|
1870
|
-
const
|
|
1871
|
-
let bestMsgIndex = -1;
|
|
1872
|
-
let bestLine = -1;
|
|
1873
|
-
for (let mi = 0; mi < messages.length; mi++) {
|
|
1874
|
-
if (
|
|
1875
|
-
messages[mi].lineNumber < note.lineNumber &&
|
|
1876
|
-
messages[mi].lineNumber > bestLine &&
|
|
1877
|
-
!hiddenMsgIndices.has(mi)
|
|
1878
|
-
) {
|
|
1879
|
-
bestLine = messages[mi].lineNumber;
|
|
1880
|
-
bestMsgIndex = mi;
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
if (bestMsgIndex < 0) return -1;
|
|
1884
|
-
return msgToFirstStep.get(bestMsgIndex) ?? -1;
|
|
1885
|
-
};
|
|
1968
|
+
const collapsedNoteFill = mix(palette.textMuted, palette.bg, 15);
|
|
1886
1969
|
|
|
1887
1970
|
const renderNoteElements = (els: SequenceElement[]): void => {
|
|
1888
1971
|
for (const el of els) {
|
|
1889
1972
|
if (isSequenceNote(el)) {
|
|
1890
1973
|
const px = participantX.get(el.participantId);
|
|
1891
1974
|
if (px === undefined) continue;
|
|
1892
|
-
const
|
|
1893
|
-
if (
|
|
1894
|
-
const noteY = stepY(si);
|
|
1895
|
-
|
|
1896
|
-
const wrappedLines = wrapTextLines(el.text, NOTE_CHARS_PER_LINE);
|
|
1897
|
-
const noteH = wrappedLines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
|
|
1898
|
-
const maxLineLen = Math.max(...wrappedLines.map((l) => l.length));
|
|
1899
|
-
const noteW = Math.min(
|
|
1900
|
-
NOTE_MAX_W,
|
|
1901
|
-
Math.max(80, maxLineLen * NOTE_CHAR_W + NOTE_PAD_H * 2 + NOTE_FOLD)
|
|
1902
|
-
);
|
|
1903
|
-
const isRight = el.position === 'right';
|
|
1904
|
-
const noteX = isRight
|
|
1905
|
-
? px + ACTIVATION_WIDTH + NOTE_GAP
|
|
1906
|
-
: px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
|
|
1907
|
-
const noteTopY = noteY + NOTE_OFFSET_BELOW;
|
|
1908
|
-
|
|
1909
|
-
// Wrap in <g> with data attributes for toggle support
|
|
1910
|
-
const noteG = svg
|
|
1911
|
-
.append('g')
|
|
1912
|
-
.attr('class', 'note')
|
|
1913
|
-
.attr('data-note-toggle', '')
|
|
1914
|
-
.attr('data-line-number', String(el.lineNumber))
|
|
1915
|
-
.attr('data-line-end', String(el.endLineNumber));
|
|
1916
|
-
|
|
1917
|
-
// Folded-corner path
|
|
1918
|
-
noteG
|
|
1919
|
-
.append('path')
|
|
1920
|
-
.attr(
|
|
1921
|
-
'd',
|
|
1922
|
-
[
|
|
1923
|
-
`M ${noteX} ${noteTopY}`,
|
|
1924
|
-
`L ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
|
|
1925
|
-
`L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
|
|
1926
|
-
`L ${noteX + noteW} ${noteTopY + noteH}`,
|
|
1927
|
-
`L ${noteX} ${noteTopY + noteH}`,
|
|
1928
|
-
'Z',
|
|
1929
|
-
].join(' ')
|
|
1930
|
-
)
|
|
1931
|
-
.attr('fill', noteFill)
|
|
1932
|
-
.attr('stroke', palette.textMuted)
|
|
1933
|
-
.attr('stroke-width', 0.75)
|
|
1934
|
-
.attr('class', 'note-box');
|
|
1975
|
+
const noteTopY = noteYMap.get(el);
|
|
1976
|
+
if (noteTopY === undefined) continue;
|
|
1935
1977
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
.append('path')
|
|
1939
|
-
.attr(
|
|
1940
|
-
'd',
|
|
1941
|
-
[
|
|
1942
|
-
`M ${noteX + noteW - NOTE_FOLD} ${noteTopY}`,
|
|
1943
|
-
`L ${noteX + noteW - NOTE_FOLD} ${noteTopY + NOTE_FOLD}`,
|
|
1944
|
-
`L ${noteX + noteW} ${noteTopY + NOTE_FOLD}`,
|
|
1945
|
-
].join(' ')
|
|
1946
|
-
)
|
|
1947
|
-
.attr('fill', 'none')
|
|
1948
|
-
.attr('stroke', palette.textMuted)
|
|
1949
|
-
.attr('stroke-width', 0.75)
|
|
1950
|
-
.attr('class', 'note-fold');
|
|
1951
|
-
|
|
1952
|
-
// Render text with inline markdown
|
|
1953
|
-
wrappedLines.forEach((line, li) => {
|
|
1954
|
-
const textY =
|
|
1955
|
-
noteTopY + NOTE_PAD_V + (li + 1) * NOTE_LINE_H - 3;
|
|
1956
|
-
const isBullet = line.startsWith('- ');
|
|
1957
|
-
const bulletIndent = isBullet ? 10 : 0;
|
|
1958
|
-
const displayLine = isBullet ? line.slice(2) : line;
|
|
1959
|
-
const textEl = noteG
|
|
1960
|
-
.append('text')
|
|
1961
|
-
.attr('x', noteX + NOTE_PAD_H + bulletIndent)
|
|
1962
|
-
.attr('y', textY)
|
|
1963
|
-
.attr('fill', palette.text)
|
|
1964
|
-
.attr('font-size', NOTE_FONT_SIZE)
|
|
1965
|
-
.attr('class', 'note-text');
|
|
1978
|
+
const expanded = isNoteExpanded(el);
|
|
1979
|
+
const isRight = el.position === 'right';
|
|
1966
1980
|
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
|
1969
2044
|
.append('text')
|
|
1970
|
-
.attr('x', noteX + NOTE_PAD_H)
|
|
2045
|
+
.attr('x', noteX + NOTE_PAD_H + bulletIndent)
|
|
1971
2046
|
.attr('y', textY)
|
|
1972
2047
|
.attr('fill', palette.text)
|
|
1973
2048
|
.attr('font-size', NOTE_FONT_SIZE)
|
|
1974
|
-
.
|
|
1975
|
-
|
|
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
|
+
}
|
|
1976
2060
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
+
}
|
|
1997
2082
|
}
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
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
|
+
}
|
|
2000
2145
|
} else if (isSequenceBlock(el)) {
|
|
2001
2146
|
renderNoteElements(el.children);
|
|
2002
2147
|
if (el.elseIfBranches) {
|
|
@@ -2014,6 +2159,40 @@ export function renderSequenceDiagram(
|
|
|
2014
2159
|
}
|
|
2015
2160
|
}
|
|
2016
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
|
+
|
|
2017
2196
|
function renderParticipant(
|
|
2018
2197
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
2019
2198
|
participant: SequenceParticipant,
|