@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.
- package/dist/cli.cjs +92 -131
- package/dist/editor.cjs +13 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +13 -1
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +13 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +13 -1
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +4524 -1511
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +427 -186
- package/dist/index.d.ts +427 -186
- package/dist/index.js +4526 -1503
- package/dist/index.js.map +1 -1
- package/docs/guide/chart-mindmap.md +198 -0
- package/docs/guide/chart-sequence.md +23 -1
- package/docs/guide/chart-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/language-reference.md +210 -2
- package/package.json +22 -9
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +51 -9
- package/src/boxes-and-lines/parser.ts +16 -4
- package/src/boxes-and-lines/renderer.ts +121 -23
- package/src/boxes-and-lines/types.ts +1 -0
- package/src/c4/parser.ts +8 -7
- package/src/class/parser.ts +6 -0
- package/src/cli.ts +1 -9
- package/src/completion.ts +26 -0
- package/src/d3.ts +169 -266
- package/src/dgmo-router.ts +103 -5
- package/src/diagnostics.ts +16 -6
- package/src/echarts.ts +43 -10
- package/src/editor/keywords.ts +12 -0
- package/src/er/parser.ts +22 -2
- package/src/gantt/renderer.ts +2 -2
- package/src/graph/flowchart-parser.ts +89 -52
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-parser.ts +60 -35
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +41 -16
- package/src/infra/parser.ts +9 -2
- package/src/kanban/renderer.ts +305 -59
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +379 -0
- package/src/mindmap/renderer.ts +543 -0
- package/src/mindmap/text-wrap.ts +207 -0
- package/src/mindmap/types.ts +55 -0
- package/src/palettes/color-utils.ts +4 -12
- package/src/palettes/index.ts +0 -4
- package/src/render.ts +31 -20
- package/src/sequence/parser.ts +7 -2
- package/src/sequence/renderer.ts +141 -21
- package/src/sharing.ts +2 -0
- package/src/sitemap/layout.ts +35 -12
- package/src/sitemap/renderer.ts +1 -6
- package/src/utils/arrows.ts +180 -11
- package/src/utils/d3-types.ts +4 -0
- package/src/utils/export-container.ts +3 -2
- package/src/utils/legend-constants.ts +0 -4
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +2 -2
- package/src/utils/parsing.ts +2 -0
- package/src/utils/time-ticks.ts +213 -0
- package/src/wireframe/layout.ts +460 -0
- package/src/wireframe/parser.ts +956 -0
- package/src/wireframe/renderer.ts +1293 -0
- package/src/wireframe/types.ts +110 -0
- package/src/branding.ts +0 -67
- package/src/dgmo-mermaid.ts +0 -262
- package/src/palettes/mermaid-bridge.ts +0 -220
package/src/sequence/renderer.ts
CHANGED
|
@@ -60,6 +60,11 @@ const NOTE_CHARS_PER_LINE = Math.floor(
|
|
|
60
60
|
);
|
|
61
61
|
const COLLAPSED_NOTE_H = 20;
|
|
62
62
|
const COLLAPSED_NOTE_W = 40;
|
|
63
|
+
const ACTIVATION_WIDTH = 10;
|
|
64
|
+
const SELF_CALL_HEIGHT = 25;
|
|
65
|
+
const SELF_CALL_WIDTH = 30;
|
|
66
|
+
// Max note width that keeps a note within one participant lane
|
|
67
|
+
const NOTE_LANE_MAX = PARTICIPANT_GAP - ACTIVATION_WIDTH - NOTE_GAP; // 135px
|
|
63
68
|
|
|
64
69
|
function wrapTextLines(text: string, maxChars: number): string[] {
|
|
65
70
|
const rawLines = text.split('\n');
|
|
@@ -953,6 +958,37 @@ export function renderSequenceDiagram(
|
|
|
953
958
|
);
|
|
954
959
|
if (participants.length === 0) return;
|
|
955
960
|
|
|
961
|
+
// Participant index lookup — used to clamp note width within one lane
|
|
962
|
+
const participantIndexMap = new Map<string, number>();
|
|
963
|
+
participants.forEach((p, i) => participantIndexMap.set(p.id, i));
|
|
964
|
+
|
|
965
|
+
// Extra X shift for notes after self-calls
|
|
966
|
+
const SELF_CALL_NOTE_X_SHIFT =
|
|
967
|
+
ACTIVATION_WIDTH / 2 +
|
|
968
|
+
SELF_CALL_WIDTH +
|
|
969
|
+
NOTE_GAP -
|
|
970
|
+
(ACTIVATION_WIDTH + NOTE_GAP); // 25px
|
|
971
|
+
|
|
972
|
+
const noteEffectiveMaxW = (
|
|
973
|
+
participantId: string,
|
|
974
|
+
position: 'right' | 'left',
|
|
975
|
+
afterSelfCall = false
|
|
976
|
+
): number => {
|
|
977
|
+
const idx = participantIndexMap.get(participantId);
|
|
978
|
+
if (idx === undefined) return NOTE_MAX_W;
|
|
979
|
+
const hasNeighbor =
|
|
980
|
+
position === 'right' ? idx < participants.length - 1 : idx > 0;
|
|
981
|
+
if (!hasNeighbor) return NOTE_MAX_W;
|
|
982
|
+
const laneMax =
|
|
983
|
+
afterSelfCall && position === 'right'
|
|
984
|
+
? NOTE_LANE_MAX - SELF_CALL_NOTE_X_SHIFT
|
|
985
|
+
: NOTE_LANE_MAX;
|
|
986
|
+
return Math.min(NOTE_MAX_W, laneMax);
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
const charsForWidth = (maxW: number): number =>
|
|
990
|
+
Math.floor((maxW - NOTE_PAD_H * 2 - NOTE_FOLD) / NOTE_CHAR_W);
|
|
991
|
+
|
|
956
992
|
const activationsOff = parsedOptions.activations?.toLowerCase() === 'off';
|
|
957
993
|
|
|
958
994
|
// Tag resolution — shared utility handles priority chain:
|
|
@@ -1049,7 +1085,39 @@ export function renderSequenceDiagram(
|
|
|
1049
1085
|
return msgToLastStep.get(closestMsgIndex) ?? -1;
|
|
1050
1086
|
};
|
|
1051
1087
|
|
|
1052
|
-
//
|
|
1088
|
+
// Check whether a note's preceding message is a self-call.
|
|
1089
|
+
// Self-call loopback arrows extend SELF_CALL_HEIGHT below the step Y,
|
|
1090
|
+
// so notes after self-calls need a larger vertical offset.
|
|
1091
|
+
const isNoteAfterSelfCall = (note: SequenceNote): boolean => {
|
|
1092
|
+
let closestMsgIndex = -1;
|
|
1093
|
+
let closestLine = -1;
|
|
1094
|
+
for (let mi = 0; mi < messages.length; mi++) {
|
|
1095
|
+
if (
|
|
1096
|
+
messages[mi].lineNumber < note.lineNumber &&
|
|
1097
|
+
messages[mi].lineNumber > closestLine
|
|
1098
|
+
) {
|
|
1099
|
+
closestLine = messages[mi].lineNumber;
|
|
1100
|
+
closestMsgIndex = mi;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (closestMsgIndex < 0) return false;
|
|
1104
|
+
const msg = messages[closestMsgIndex];
|
|
1105
|
+
return msg.from === msg.to;
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
// Extra gap below self-call loop before note starts
|
|
1109
|
+
const SELF_CALL_NOTE_GAP = 8;
|
|
1110
|
+
const noteOffsetBelow = (note: SequenceNote): number =>
|
|
1111
|
+
isNoteAfterSelfCall(note)
|
|
1112
|
+
? SELF_CALL_HEIGHT + NOTE_OFFSET_BELOW + SELF_CALL_NOTE_GAP
|
|
1113
|
+
: NOTE_OFFSET_BELOW;
|
|
1114
|
+
|
|
1115
|
+
// Find the first visible message index in an element subtree.
|
|
1116
|
+
// Use lineNumber lookup instead of indexOf — collapse projection creates
|
|
1117
|
+
// separate spread copies for messages[] and elements[], breaking reference equality.
|
|
1118
|
+
const msgLineToIdx = new Map<number, number>();
|
|
1119
|
+
messages.forEach((m, i) => msgLineToIdx.set(m.lineNumber, i));
|
|
1120
|
+
|
|
1053
1121
|
const findFirstMsgIndex = (els: SequenceElement[]): number => {
|
|
1054
1122
|
for (const el of els) {
|
|
1055
1123
|
if (isSequenceBlock(el)) {
|
|
@@ -1064,7 +1132,7 @@ export function renderSequenceDiagram(
|
|
|
1064
1132
|
const elseIdx = findFirstMsgIndex(el.elseChildren);
|
|
1065
1133
|
if (elseIdx >= 0) return elseIdx;
|
|
1066
1134
|
} else if (!isSequenceSection(el) && !isSequenceNote(el)) {
|
|
1067
|
-
const idx =
|
|
1135
|
+
const idx = msgLineToIdx.get(el.lineNumber) ?? -1;
|
|
1068
1136
|
if (idx >= 0 && !hiddenMsgIndices.has(idx)) return idx;
|
|
1069
1137
|
}
|
|
1070
1138
|
}
|
|
@@ -1121,8 +1189,11 @@ export function renderSequenceDiagram(
|
|
|
1121
1189
|
// When notes share horizontal space with subsequent arrows, generous vertical clearance
|
|
1122
1190
|
// is needed so note boxes don't visually cover message labels.
|
|
1123
1191
|
const NOTE_TRAILING_GAP = 35;
|
|
1124
|
-
const computeNoteHeight = (
|
|
1125
|
-
|
|
1192
|
+
const computeNoteHeight = (
|
|
1193
|
+
text: string,
|
|
1194
|
+
maxChars: number = NOTE_CHARS_PER_LINE
|
|
1195
|
+
): number => {
|
|
1196
|
+
const lines = wrapTextLines(text, maxChars);
|
|
1126
1197
|
return lines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
|
|
1127
1198
|
};
|
|
1128
1199
|
let trailingNoteSpace = 0; // extra space for notes at the end with no following message
|
|
@@ -1131,15 +1202,18 @@ export function renderSequenceDiagram(
|
|
|
1131
1202
|
const el = els[i];
|
|
1132
1203
|
if (isSequenceNote(el)) {
|
|
1133
1204
|
// Total vertical extent of notes from the message arrow:
|
|
1134
|
-
//
|
|
1205
|
+
// offset (gap above first note — larger after self-calls)
|
|
1135
1206
|
// + each note's height + NOTE_OFFSET_BELOW (inter-note gap)
|
|
1136
1207
|
// + NOTE_TRAILING_GAP (gap below last note — clears next message label)
|
|
1137
|
-
|
|
1208
|
+
const firstOffset = noteOffsetBelow(el as SequenceNote);
|
|
1209
|
+
let totalExtent = firstOffset;
|
|
1138
1210
|
let j = i;
|
|
1139
1211
|
while (j < els.length && isSequenceNote(els[j])) {
|
|
1140
1212
|
const note = els[j] as SequenceNote;
|
|
1213
|
+
const sc = isNoteAfterSelfCall(note);
|
|
1214
|
+
const maxW = noteEffectiveMaxW(note.participantId, note.position, sc);
|
|
1141
1215
|
const noteH = isNoteExpanded(note)
|
|
1142
|
-
? computeNoteHeight(note.text)
|
|
1216
|
+
? computeNoteHeight(note.text, charsForWidth(maxW))
|
|
1143
1217
|
: COLLAPSED_NOTE_H;
|
|
1144
1218
|
totalExtent += noteH + NOTE_OFFSET_BELOW;
|
|
1145
1219
|
j++;
|
|
@@ -1401,13 +1475,18 @@ export function renderSequenceDiagram(
|
|
|
1401
1475
|
let noteTopY: number;
|
|
1402
1476
|
if (prevNoteY !== undefined && prevNote) {
|
|
1403
1477
|
// Stack below previous note
|
|
1478
|
+
const prevMaxW = noteEffectiveMaxW(
|
|
1479
|
+
prevNote.participantId,
|
|
1480
|
+
prevNote.position,
|
|
1481
|
+
isNoteAfterSelfCall(prevNote)
|
|
1482
|
+
);
|
|
1404
1483
|
const prevNoteH = isNoteExpanded(prevNote)
|
|
1405
|
-
? computeNoteHeight(prevNote.text)
|
|
1484
|
+
? computeNoteHeight(prevNote.text, charsForWidth(prevMaxW))
|
|
1406
1485
|
: COLLAPSED_NOTE_H;
|
|
1407
1486
|
noteTopY = prevNoteY + prevNoteH + NOTE_OFFSET_BELOW;
|
|
1408
1487
|
} else {
|
|
1409
|
-
// First note after a message
|
|
1410
|
-
noteTopY = stepY(si) +
|
|
1488
|
+
// First note after a message — use larger offset after self-calls
|
|
1489
|
+
noteTopY = stepY(si) + noteOffsetBelow(el);
|
|
1411
1490
|
}
|
|
1412
1491
|
noteYMap.set(el, noteTopY);
|
|
1413
1492
|
} else if (isSequenceBlock(el)) {
|
|
@@ -1435,8 +1514,13 @@ export function renderSequenceDiagram(
|
|
|
1435
1514
|
)
|
|
1436
1515
|
: layoutEndY;
|
|
1437
1516
|
for (const [note, noteTopY] of noteYMap) {
|
|
1517
|
+
const maxW = noteEffectiveMaxW(
|
|
1518
|
+
note.participantId,
|
|
1519
|
+
note.position,
|
|
1520
|
+
isNoteAfterSelfCall(note)
|
|
1521
|
+
);
|
|
1438
1522
|
const noteH = isNoteExpanded(note)
|
|
1439
|
-
? computeNoteHeight(note.text)
|
|
1523
|
+
? computeNoteHeight(note.text, charsForWidth(maxW))
|
|
1440
1524
|
: COLLAPSED_NOTE_H;
|
|
1441
1525
|
contentBottomY = Math.max(
|
|
1442
1526
|
contentBottomY,
|
|
@@ -2093,7 +2177,6 @@ export function renderSequenceDiagram(
|
|
|
2093
2177
|
}
|
|
2094
2178
|
|
|
2095
2179
|
// Render activation rectangles (behind arrows)
|
|
2096
|
-
const ACTIVATION_WIDTH = 10;
|
|
2097
2180
|
const ACTIVATION_NEST_OFFSET = 6;
|
|
2098
2181
|
activations.forEach((act) => {
|
|
2099
2182
|
const px = participantX.get(act.participantId);
|
|
@@ -2285,8 +2368,7 @@ export function renderSequenceDiagram(
|
|
|
2285
2368
|
}
|
|
2286
2369
|
|
|
2287
2370
|
// Render steps (calls and returns in stack-inferred order)
|
|
2288
|
-
|
|
2289
|
-
const SELF_CALL_HEIGHT = 25;
|
|
2371
|
+
// SELF_CALL_WIDTH is now a module-level constant
|
|
2290
2372
|
renderSteps.forEach((step, i) => {
|
|
2291
2373
|
const fromX = participantX.get(step.from);
|
|
2292
2374
|
const toX = participantX.get(step.to);
|
|
@@ -2354,6 +2436,10 @@ export function renderSequenceDiagram(
|
|
|
2354
2436
|
.attr('y', y + SELF_CALL_HEIGHT / 2 + 4)
|
|
2355
2437
|
.attr('text-anchor', 'start')
|
|
2356
2438
|
.attr('fill', arrowColor)
|
|
2439
|
+
.attr('paint-order', 'stroke fill')
|
|
2440
|
+
.attr('stroke', palette.bg)
|
|
2441
|
+
.attr('stroke-width', 4)
|
|
2442
|
+
.attr('stroke-linejoin', 'round')
|
|
2357
2443
|
.attr('font-size', 12)
|
|
2358
2444
|
.attr('class', 'message-label')
|
|
2359
2445
|
.attr(
|
|
@@ -2365,7 +2451,9 @@ export function renderSequenceDiagram(
|
|
|
2365
2451
|
if (tagKey && msgTagValue) {
|
|
2366
2452
|
labelEl.attr(`data-tag-${tagKey}`, msgTagValue.toLowerCase());
|
|
2367
2453
|
}
|
|
2368
|
-
|
|
2454
|
+
// TD-1: in-arrow labels render as plain text (no markdown interpretation).
|
|
2455
|
+
// Fixes the `location[]`-style silent character drop.
|
|
2456
|
+
labelEl.text(step.label);
|
|
2369
2457
|
}
|
|
2370
2458
|
} else {
|
|
2371
2459
|
// Normal call arrow — snap to activation box edges
|
|
@@ -2422,6 +2510,10 @@ export function renderSequenceDiagram(
|
|
|
2422
2510
|
.attr('y', y - 8)
|
|
2423
2511
|
.attr('text-anchor', 'middle')
|
|
2424
2512
|
.attr('fill', arrowColor)
|
|
2513
|
+
.attr('paint-order', 'stroke fill')
|
|
2514
|
+
.attr('stroke', palette.bg)
|
|
2515
|
+
.attr('stroke-width', 4)
|
|
2516
|
+
.attr('stroke-linejoin', 'round')
|
|
2425
2517
|
.attr('font-size', 12)
|
|
2426
2518
|
.attr('class', 'message-label')
|
|
2427
2519
|
.attr(
|
|
@@ -2433,7 +2525,9 @@ export function renderSequenceDiagram(
|
|
|
2433
2525
|
if (tagKey && msgTagValue) {
|
|
2434
2526
|
labelEl.attr(`data-tag-${tagKey}`, msgTagValue.toLowerCase());
|
|
2435
2527
|
}
|
|
2436
|
-
|
|
2528
|
+
// TD-1: in-arrow labels render as plain text (no markdown interpretation).
|
|
2529
|
+
// Fixes the `location[]`-style silent character drop.
|
|
2530
|
+
labelEl.text(step.label);
|
|
2437
2531
|
}
|
|
2438
2532
|
}
|
|
2439
2533
|
} else {
|
|
@@ -2494,6 +2588,10 @@ export function renderSequenceDiagram(
|
|
|
2494
2588
|
.attr('y', y - 6)
|
|
2495
2589
|
.attr('text-anchor', 'middle')
|
|
2496
2590
|
.attr('fill', returnColor)
|
|
2591
|
+
.attr('paint-order', 'stroke fill')
|
|
2592
|
+
.attr('stroke', palette.bg)
|
|
2593
|
+
.attr('stroke-width', 4)
|
|
2594
|
+
.attr('stroke-linejoin', 'round')
|
|
2497
2595
|
.attr('font-size', 11)
|
|
2498
2596
|
.attr('class', 'message-label')
|
|
2499
2597
|
.attr(
|
|
@@ -2505,7 +2603,12 @@ export function renderSequenceDiagram(
|
|
|
2505
2603
|
if (tagKey && msgTagValue) {
|
|
2506
2604
|
labelEl.attr(`data-tag-${tagKey}`, msgTagValue.toLowerCase());
|
|
2507
2605
|
}
|
|
2508
|
-
|
|
2606
|
+
// TD-1: in-arrow labels render as plain text (no markdown
|
|
2607
|
+
// interpretation). Return-arrow labels are currently always empty
|
|
2608
|
+
// (buildRenderSequence sets them to '') but this path is kept in
|
|
2609
|
+
// sync with the call/self-call sites above to prevent a future
|
|
2610
|
+
// change resurrecting the location[] silent-drop bug.
|
|
2611
|
+
labelEl.text(step.label);
|
|
2509
2612
|
}
|
|
2510
2613
|
}
|
|
2511
2614
|
});
|
|
@@ -2530,15 +2633,27 @@ export function renderSequenceDiagram(
|
|
|
2530
2633
|
|
|
2531
2634
|
if (expanded) {
|
|
2532
2635
|
// --- Expanded note: full folded-corner box with wrapped text ---
|
|
2533
|
-
const
|
|
2636
|
+
const afterSelfCall = isNoteAfterSelfCall(el);
|
|
2637
|
+
const maxW = noteEffectiveMaxW(
|
|
2638
|
+
el.participantId,
|
|
2639
|
+
el.position,
|
|
2640
|
+
afterSelfCall
|
|
2641
|
+
);
|
|
2642
|
+
const maxChars = charsForWidth(maxW);
|
|
2643
|
+
const wrappedLines = wrapTextLines(el.text, maxChars);
|
|
2534
2644
|
const noteH = wrappedLines.length * NOTE_LINE_H + NOTE_PAD_V * 2;
|
|
2535
2645
|
const maxLineLen = Math.max(...wrappedLines.map((l) => l.length));
|
|
2536
2646
|
const noteW = Math.min(
|
|
2537
|
-
|
|
2647
|
+
maxW,
|
|
2538
2648
|
Math.max(80, maxLineLen * NOTE_CHAR_W + NOTE_PAD_H * 2 + NOTE_FOLD)
|
|
2539
2649
|
);
|
|
2650
|
+
// Shift notes past self-call loopback when applicable
|
|
2651
|
+
const rightOffset =
|
|
2652
|
+
afterSelfCall && isRight
|
|
2653
|
+
? ACTIVATION_WIDTH / 2 + SELF_CALL_WIDTH + NOTE_GAP
|
|
2654
|
+
: ACTIVATION_WIDTH + NOTE_GAP;
|
|
2540
2655
|
const noteX = isRight
|
|
2541
|
-
? px +
|
|
2656
|
+
? px + rightOffset
|
|
2542
2657
|
: px - ACTIVATION_WIDTH - NOTE_GAP - noteW;
|
|
2543
2658
|
|
|
2544
2659
|
const noteG = svg
|
|
@@ -2612,8 +2727,13 @@ export function renderSequenceDiagram(
|
|
|
2612
2727
|
} else {
|
|
2613
2728
|
// --- Collapsed note: compact indicator ---
|
|
2614
2729
|
const cFold = 6;
|
|
2730
|
+
const afterSelfCallC = isNoteAfterSelfCall(el);
|
|
2731
|
+
const rightOffsetC =
|
|
2732
|
+
afterSelfCallC && isRight
|
|
2733
|
+
? ACTIVATION_WIDTH / 2 + SELF_CALL_WIDTH + NOTE_GAP
|
|
2734
|
+
: ACTIVATION_WIDTH + NOTE_GAP;
|
|
2615
2735
|
const noteX = isRight
|
|
2616
|
-
? px +
|
|
2736
|
+
? px + rightOffsetC
|
|
2617
2737
|
: px - ACTIVATION_WIDTH - NOTE_GAP - COLLAPSED_NOTE_W;
|
|
2618
2738
|
|
|
2619
2739
|
const noteG = svg
|
package/src/sharing.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface CompactViewState {
|
|
|
30
30
|
rps?: number; // RPS multiplier (infra)
|
|
31
31
|
spd?: number; // playback speed (infra)
|
|
32
32
|
io?: Record<string, number>; // instance overrides (infra)
|
|
33
|
+
hd?: boolean; // hide descriptions (mindmap)
|
|
34
|
+
cbd?: boolean; // color by depth (mindmap)
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export interface DecodedDiagramUrl {
|
package/src/sitemap/layout.ts
CHANGED
|
@@ -396,15 +396,15 @@ export function layoutSitemap(
|
|
|
396
396
|
const pageNodeIds = new Set<string>();
|
|
397
397
|
const collapsedContainerIds = new Set<string>();
|
|
398
398
|
|
|
399
|
-
// Identify containers vs pages, and detect collapsed
|
|
399
|
+
// Identify containers vs pages, and detect collapsed containers.
|
|
400
|
+
// A container is collapsed iff hiddenCounts records it with a positive count
|
|
401
|
+
// (meaning collapseSitemapTree pruned its descendants). Source-level empty
|
|
402
|
+
// containers (never had children) are NOT treated as collapsed.
|
|
400
403
|
for (const flat of flatNodes) {
|
|
401
404
|
if (flat.sitemapNode.isContainer) {
|
|
402
405
|
containerIds.add(flat.sitemapNode.id);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
(f) => f.parentContainerId === flat.sitemapNode.id
|
|
406
|
-
);
|
|
407
|
-
if (!hasAnyChild) {
|
|
406
|
+
const hidden = hiddenCounts?.get(flat.sitemapNode.id) ?? 0;
|
|
407
|
+
if (hidden > 0) {
|
|
408
408
|
collapsedContainerIds.add(flat.sitemapNode.id);
|
|
409
409
|
}
|
|
410
410
|
} else {
|
|
@@ -412,16 +412,31 @@ export function layoutSitemap(
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
// Sibling-page floor for collapsed containers — prevents collapsed containers
|
|
416
|
+
// from rendering smaller than meta-rich page cards in the same layout.
|
|
417
|
+
let pageMaxW = 0;
|
|
418
|
+
let pageMaxH = 0;
|
|
419
|
+
for (const f of flatNodes) {
|
|
420
|
+
if (!f.sitemapNode.isContainer) {
|
|
421
|
+
if (f.width > pageMaxW) pageMaxW = f.width;
|
|
422
|
+
if (f.height > pageMaxH) pageMaxH = f.height;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
415
426
|
// Add nodes to dagre
|
|
416
427
|
for (const flat of flatNodes) {
|
|
417
428
|
const node = flat.sitemapNode;
|
|
418
429
|
if (node.isContainer) {
|
|
419
430
|
if (collapsedContainerIds.has(node.id)) {
|
|
420
|
-
// Collapsed container — regular node with explicit dimensions
|
|
431
|
+
// Collapsed container — regular node with explicit dimensions.
|
|
432
|
+
// Floor to max page-card dims so collapsed containers never look
|
|
433
|
+
// smaller than sibling page cards.
|
|
434
|
+
const flooredW = Math.max(flat.width, pageMaxW);
|
|
435
|
+
const flooredH = Math.max(flat.height, pageMaxH);
|
|
421
436
|
g.setNode(node.id, {
|
|
422
437
|
label: node.label,
|
|
423
|
-
width:
|
|
424
|
-
height:
|
|
438
|
+
width: flooredW,
|
|
439
|
+
height: flooredH,
|
|
425
440
|
});
|
|
426
441
|
} else {
|
|
427
442
|
// Regular container — compound node with padding for child layout
|
|
@@ -546,7 +561,15 @@ export function layoutSitemap(
|
|
|
546
561
|
node.children.length > 0 || (hc != null && hc > 0) || undefined,
|
|
547
562
|
});
|
|
548
563
|
} else {
|
|
549
|
-
// Fallback
|
|
564
|
+
// Fallback — still apply the floor for consistency
|
|
565
|
+
const isCollapsed = collapsedContainerIds.has(node.id);
|
|
566
|
+
const flooredW = isCollapsed
|
|
567
|
+
? Math.max(flat.width, pageMaxW)
|
|
568
|
+
: flat.width;
|
|
569
|
+
const fallbackH = isCollapsed
|
|
570
|
+
? flat.height
|
|
571
|
+
: labelHeight + CONTAINER_PAD_BOTTOM;
|
|
572
|
+
const flooredH = isCollapsed ? Math.max(fallbackH, pageMaxH) : fallbackH;
|
|
550
573
|
layoutContainers.push({
|
|
551
574
|
nodeId: node.id,
|
|
552
575
|
label: node.label,
|
|
@@ -556,8 +579,8 @@ export function layoutSitemap(
|
|
|
556
579
|
tagMetadata: flat.fullMeta,
|
|
557
580
|
x: MARGIN,
|
|
558
581
|
y: MARGIN,
|
|
559
|
-
width:
|
|
560
|
-
height:
|
|
582
|
+
width: flooredW,
|
|
583
|
+
height: flooredH,
|
|
561
584
|
labelHeight,
|
|
562
585
|
hiddenCount: hc,
|
|
563
586
|
hasChildren:
|
package/src/sitemap/renderer.ts
CHANGED
|
@@ -704,8 +704,6 @@ export async function renderSitemapForExport(
|
|
|
704
704
|
const { parseSitemap } = await import('./parser');
|
|
705
705
|
const { layoutSitemap } = await import('./layout');
|
|
706
706
|
const { getPalette } = await import('../palettes');
|
|
707
|
-
const { injectBranding } = await import('../branding');
|
|
708
|
-
|
|
709
707
|
const isDark = theme === 'dark';
|
|
710
708
|
const effectivePalette =
|
|
711
709
|
palette ?? (isDark ? getPalette('nord').dark : getPalette('nord').light);
|
|
@@ -756,8 +754,5 @@ export async function renderSitemapForExport(
|
|
|
756
754
|
|
|
757
755
|
const svgHtml = svgEl.outerHTML;
|
|
758
756
|
document.body.removeChild(container);
|
|
759
|
-
|
|
760
|
-
const brandColor =
|
|
761
|
-
theme === 'transparent' ? '#888' : effectivePalette.textMuted;
|
|
762
|
-
return injectBranding(svgHtml, brandColor);
|
|
757
|
+
return svgHtml;
|
|
763
758
|
}
|
package/src/utils/arrows.ts
CHANGED
|
@@ -5,6 +5,24 @@
|
|
|
5
5
|
// Labeled arrow syntax (always left-to-right):
|
|
6
6
|
// Sync: `-label->`
|
|
7
7
|
// Async: `~label~>`
|
|
8
|
+
//
|
|
9
|
+
// In-arrow label character-set contract (see docs/dgmo-language-spec.md
|
|
10
|
+
// §"In-Arrow Message Labels"):
|
|
11
|
+
// - Allowed: any codepoint except the forbidden substrings and forbidden
|
|
12
|
+
// control characters below.
|
|
13
|
+
// - Forbidden substrings: "->", "~>" (arrow-token lookalikes inside labels).
|
|
14
|
+
// Use the post-colon form for labels that need these symbols:
|
|
15
|
+
// `A -> B: uses -> to chain`
|
|
16
|
+
// - Forbidden characters: C0 control chars U+0000–U+001F EXCEPT U+0009 (tab),
|
|
17
|
+
// and U+007F (DEL).
|
|
18
|
+
// - Whitespace: leading/trailing trimmed; internal runs (incl. tab, NBSP,
|
|
19
|
+
// ZWSP) preserved — never collapsed.
|
|
20
|
+
// - Plain text only: no markdown interpretation. `*`, `_`, backticks,
|
|
21
|
+
// `[`, `]`, `{`, `}` are literal characters.
|
|
22
|
+
|
|
23
|
+
import type { DgmoError } from '../diagnostics';
|
|
24
|
+
import { makeDgmoError } from '../diagnostics';
|
|
25
|
+
import { RECOGNIZED_COLOR_NAMES } from '../colors';
|
|
8
26
|
|
|
9
27
|
interface ParsedArrow {
|
|
10
28
|
from: string;
|
|
@@ -13,6 +31,160 @@ interface ParsedArrow {
|
|
|
13
31
|
async: boolean;
|
|
14
32
|
}
|
|
15
33
|
|
|
34
|
+
// ============================================================
|
|
35
|
+
// Diagnostic codes (TD-16)
|
|
36
|
+
// ============================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stable diagnostic codes for in-arrow label parsing errors.
|
|
40
|
+
*
|
|
41
|
+
* **Active codes** — emitted by the parser pipeline today:
|
|
42
|
+
* - `ARROW_SUBSTRING_IN_LABEL` (TD-13)
|
|
43
|
+
* - `CONTROL_CHAR_IN_LABEL` (TD-14)
|
|
44
|
+
*
|
|
45
|
+
* **Reserved codes** — declared but NOT currently emitted. These are
|
|
46
|
+
* placeholders for future tightening of the arrow-tokenization rules
|
|
47
|
+
* described in TD-9. Today's chart parsers catch these cases through
|
|
48
|
+
* their own regex machinery with different diagnostics. A follow-up
|
|
49
|
+
* spec that introduces a dedicated tokenizer can start emitting them
|
|
50
|
+
* without changing the public code shape:
|
|
51
|
+
* - `TRAILING_ARROW_TEXT` — extra `->`/`~>` after the primary arrow
|
|
52
|
+
* - `MIXED_ARROW_DELIMITERS` — opening delim type doesn't match arrow
|
|
53
|
+
*
|
|
54
|
+
* See `docs/dgmo-language-spec-decisions.md` → TD-16 for the rationale.
|
|
55
|
+
*/
|
|
56
|
+
export const ARROW_DIAGNOSTIC_CODES = {
|
|
57
|
+
/** Active: label contains `->` or `~>` substring (TD-13). */
|
|
58
|
+
ARROW_SUBSTRING_IN_LABEL: 'E_ARROW_SUBSTRING_IN_LABEL',
|
|
59
|
+
/** Active: label contains a forbidden control character (TD-14). */
|
|
60
|
+
CONTROL_CHAR_IN_LABEL: 'E_CONTROL_CHAR_IN_LABEL',
|
|
61
|
+
/** Reserved: not currently emitted by any parser. See JSDoc above. */
|
|
62
|
+
TRAILING_ARROW_TEXT: 'E_TRAILING_ARROW_TEXT',
|
|
63
|
+
/** Reserved: not currently emitted by any parser. See JSDoc above. */
|
|
64
|
+
MIXED_ARROW_DELIMITERS: 'E_MIXED_ARROW_DELIMITERS',
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// validateLabelCharacters (TD-13, TD-14)
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate an in-arrow label against the TD-13 and TD-14 character-set
|
|
73
|
+
* contract. Returns diagnostics (possibly empty). Does NOT mutate the label —
|
|
74
|
+
* callers that want a normalized label should trim before calling.
|
|
75
|
+
*
|
|
76
|
+
* TD-13: label must not contain the substrings "->" or "~>".
|
|
77
|
+
* TD-14: label must not contain C0 control chars other than tab, and no DEL.
|
|
78
|
+
*/
|
|
79
|
+
export function validateLabelCharacters(
|
|
80
|
+
label: string,
|
|
81
|
+
lineNumber: number
|
|
82
|
+
): DgmoError[] {
|
|
83
|
+
const out: DgmoError[] = [];
|
|
84
|
+
|
|
85
|
+
// TD-13: forbidden substrings
|
|
86
|
+
if (label.includes('->') || label.includes('~>')) {
|
|
87
|
+
out.push(
|
|
88
|
+
makeDgmoError(
|
|
89
|
+
lineNumber,
|
|
90
|
+
'Arrow symbols (-> or ~>) are not allowed inside a label. ' +
|
|
91
|
+
'Move the label after the arrow: "A -> B: uses -> to chain". ' +
|
|
92
|
+
'See "In-Arrow Message Labels" → Forbidden.',
|
|
93
|
+
'error',
|
|
94
|
+
ARROW_DIAGNOSTIC_CODES.ARROW_SUBSTRING_IN_LABEL
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// TD-14: control chars (iterate codepoints to handle surrogate pairs)
|
|
100
|
+
for (const ch of label) {
|
|
101
|
+
const cp = ch.codePointAt(0)!;
|
|
102
|
+
const isC0 = cp >= 0x00 && cp <= 0x1f && cp !== 0x09; // allow tab
|
|
103
|
+
const isDel = cp === 0x7f;
|
|
104
|
+
if (isC0 || isDel) {
|
|
105
|
+
const hex = cp.toString(16).toUpperCase().padStart(4, '0');
|
|
106
|
+
out.push(
|
|
107
|
+
makeDgmoError(
|
|
108
|
+
lineNumber,
|
|
109
|
+
`Label contains a control character (U+${hex}). ` +
|
|
110
|
+
'Remove it and use plain text.',
|
|
111
|
+
'error',
|
|
112
|
+
ARROW_DIAGNOSTIC_CODES.CONTROL_CHAR_IN_LABEL
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
break; // one diagnostic per label is enough
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================
|
|
123
|
+
// parseInArrowLabel (TD-1, TD-8, TD-10, TD-13, TD-14)
|
|
124
|
+
// ============================================================
|
|
125
|
+
|
|
126
|
+
export interface ParseInArrowLabelResult {
|
|
127
|
+
/** Cleaned label (trimmed; `undefined` if empty after trim per TD-10). */
|
|
128
|
+
label: string | undefined;
|
|
129
|
+
diagnostics: DgmoError[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Normalize and validate a raw in-arrow label.
|
|
134
|
+
*
|
|
135
|
+
* Behavior:
|
|
136
|
+
* - Trims leading/trailing whitespace (TD-8: internal whitespace preserved).
|
|
137
|
+
* - Empty-after-trim → `{ label: undefined }` (TD-10 normalization).
|
|
138
|
+
* - TD-13: emits `E_ARROW_SUBSTRING_IN_LABEL` if `->` or `~>` is present.
|
|
139
|
+
* - TD-14: emits `E_CONTROL_CHAR_IN_LABEL` for forbidden control chars.
|
|
140
|
+
*
|
|
141
|
+
* This helper is intentionally chart-agnostic: it operates on an already
|
|
142
|
+
* extracted label string, leaving each chart's existing arrow-finding
|
|
143
|
+
* tokenization in place. TD-11 color-parens is handled inside the
|
|
144
|
+
* flowchart and state `parseArrowToken` functions because those are the
|
|
145
|
+
* only charts that interpret `-(color)->` as a colored edge; they use
|
|
146
|
+
* `matchColorParens()` from this module for the shared lookup.
|
|
147
|
+
*/
|
|
148
|
+
export function parseInArrowLabel(
|
|
149
|
+
rawLabel: string,
|
|
150
|
+
lineNumber: number
|
|
151
|
+
): ParseInArrowLabelResult {
|
|
152
|
+
const trimmed = rawLabel.trim();
|
|
153
|
+
|
|
154
|
+
// TD-10: empty/whitespace-only label normalizes to undefined
|
|
155
|
+
if (trimmed.length === 0) {
|
|
156
|
+
return { label: undefined, diagnostics: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// TD-13 / TD-14 validation
|
|
160
|
+
const diagnostics = validateLabelCharacters(trimmed, lineNumber);
|
|
161
|
+
|
|
162
|
+
return { label: trimmed, diagnostics };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================
|
|
166
|
+
// matchColorParens — shared TD-11 helper for flowchart and state
|
|
167
|
+
// ============================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Test whether a string matches the TD-11 color-parens form `(colorName)`
|
|
171
|
+
* where `colorName` is one of the 11 recognized palette color names from
|
|
172
|
+
* `src/colors.ts:RECOGNIZED_COLOR_NAMES`. Returns the lowercase color name
|
|
173
|
+
* on a match, or `null` on fall-through (whole string becomes a label).
|
|
174
|
+
*
|
|
175
|
+
* Used by flowchart and state parsers to keep the color-parens recognition
|
|
176
|
+
* rule in one place — do NOT re-implement the regex in chart parsers.
|
|
177
|
+
*/
|
|
178
|
+
export function matchColorParens(content: string): string | null {
|
|
179
|
+
const m = content.match(/^\(([A-Za-z]+)\)$/);
|
|
180
|
+
if (!m) return null;
|
|
181
|
+
const candidate = m[1].toLowerCase();
|
|
182
|
+
if ((RECOGNIZED_COLOR_NAMES as readonly string[]).includes(candidate)) {
|
|
183
|
+
return candidate;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
16
188
|
// Forward (call) patterns — participant names may contain spaces, so use non-greedy (.+?)
|
|
17
189
|
const SYNC_LABELED_RE = /^(.+?)\s*-(.+)->\s*(.+)$/;
|
|
18
190
|
const ASYNC_LABELED_RE = /^(.+?)\s*~(.+)~>\s*(.+)$/;
|
|
@@ -23,8 +195,6 @@ const RETURN_ASYNC_LABELED_RE = /^(.+?)\s*<~(.+)~\s*(.+)$/;
|
|
|
23
195
|
const BIDI_SYNC_RE = /^(.+?)\s*<-(.+)->\s*(.+)$/;
|
|
24
196
|
const BIDI_ASYNC_RE = /^(.+?)\s*<~(.+)~>\s*(.+)$/;
|
|
25
197
|
|
|
26
|
-
const ARROW_CHARS = ['->', '~>'];
|
|
27
|
-
|
|
28
198
|
/**
|
|
29
199
|
* Try to parse a labeled arrow from a trimmed line.
|
|
30
200
|
*
|
|
@@ -32,6 +202,14 @@ const ARROW_CHARS = ['->', '~>'];
|
|
|
32
202
|
* - `ParsedArrow` if matched and valid
|
|
33
203
|
* - `{ error: string }` if matched but invalid (deprecated syntax)
|
|
34
204
|
* - `null` if not a labeled arrow (caller should fall through to bare patterns)
|
|
205
|
+
*
|
|
206
|
+
* Note: arrow-char-in-label validation (TD-13) is NOT performed here —
|
|
207
|
+
* callers must route the returned `label` through `parseInArrowLabel` or
|
|
208
|
+
* `validateLabelCharacters` to get the unified `E_ARROW_SUBSTRING_IN_LABEL`
|
|
209
|
+
* diagnostic with the correct code. In practice this path is unreachable
|
|
210
|
+
* because arrow regexes are greedy enough to absorb inner `->`/`~>` tokens
|
|
211
|
+
* into the source/destination captures, but the check remains at the
|
|
212
|
+
* validator level for defense in depth.
|
|
35
213
|
*/
|
|
36
214
|
export function parseArrow(
|
|
37
215
|
line: string
|
|
@@ -73,15 +251,6 @@ export function parseArrow(
|
|
|
73
251
|
// Empty label (e.g. `--> B`) — fall through to plain arrow handling
|
|
74
252
|
if (!label) return null;
|
|
75
253
|
|
|
76
|
-
// Validate: no arrow chars inside label
|
|
77
|
-
for (const arrow of ARROW_CHARS) {
|
|
78
|
-
if (label.includes(arrow)) {
|
|
79
|
-
return {
|
|
80
|
-
error: 'Arrow characters (->, ~>) are not allowed inside labels',
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
254
|
return {
|
|
86
255
|
from: m[1],
|
|
87
256
|
to: m[3],
|
|
@@ -7,7 +7,7 @@ import { FONT_FAMILY } from '../fonts';
|
|
|
7
7
|
export function runInExportContainer<T>(
|
|
8
8
|
width: number,
|
|
9
9
|
height: number,
|
|
10
|
-
fn: (container: HTMLDivElement) => T
|
|
10
|
+
fn: (container: HTMLDivElement) => T
|
|
11
11
|
): T {
|
|
12
12
|
const container = document.createElement('div');
|
|
13
13
|
container.style.width = `${width}px`;
|
|
@@ -29,12 +29,13 @@ export function runInExportContainer<T>(
|
|
|
29
29
|
*/
|
|
30
30
|
export function extractExportSvg(
|
|
31
31
|
container: HTMLElement,
|
|
32
|
-
theme: 'light' | 'dark' | 'transparent'
|
|
32
|
+
theme: 'light' | 'dark' | 'transparent'
|
|
33
33
|
): string {
|
|
34
34
|
const svgEl = container.querySelector('svg');
|
|
35
35
|
if (!svgEl) return '';
|
|
36
36
|
if (theme === 'transparent') svgEl.style.background = 'none';
|
|
37
37
|
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
38
38
|
svgEl.style.fontFamily = FONT_FAMILY;
|
|
39
|
+
svgEl.querySelectorAll('[data-export-ignore]').forEach((el) => el.remove());
|
|
39
40
|
return svgEl.outerHTML;
|
|
40
41
|
}
|
|
@@ -16,10 +16,6 @@ export const LEGEND_EYE_SIZE = 14;
|
|
|
16
16
|
export const LEGEND_EYE_GAP = 6;
|
|
17
17
|
export const LEGEND_ICON_W = 20;
|
|
18
18
|
|
|
19
|
-
// ── Spacing constants (centralized legend system) ───────────
|
|
20
|
-
export const LEGEND_TOP_PAD = 12;
|
|
21
|
-
export const LEGEND_TITLE_GAP = 8;
|
|
22
|
-
export const LEGEND_CONTENT_GAP = 12;
|
|
23
19
|
export const LEGEND_MAX_ENTRY_ROWS = 3;
|
|
24
20
|
|
|
25
21
|
// ── Proportional text measurement ────────────────────────────
|