@diagrammo/dgmo 0.8.21 → 0.8.23
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/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +145 -93
- package/dist/editor.cjs +20 -3
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +20 -3
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +15 -2
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +15 -2
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +20843 -14937
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +426 -17
- package/dist/index.d.ts +426 -17
- package/dist/index.js +20795 -14912
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +380 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +179 -0
- package/dist/internal.d.ts +179 -0
- package/dist/internal.js +337 -0
- package/dist/internal.js.map +1 -0
- package/docs/guide/chart-cycle.md +156 -0
- package/docs/guide/chart-journey-map.md +179 -0
- package/docs/guide/chart-pyramid.md +111 -0
- package/docs/guide/chart-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/registry.json +6 -0
- package/docs/language-reference.md +177 -6
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/pyramid/dikw.dgmo +17 -0
- package/gallery/fixtures/pyramid/inverted-funnel.dgmo +16 -0
- package/gallery/fixtures/pyramid/minimal.dgmo +5 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +11 -1
- package/src/boxes-and-lines/layout.ts +309 -33
- package/src/boxes-and-lines/parser.ts +86 -10
- package/src/boxes-and-lines/renderer.ts +250 -91
- package/src/boxes-and-lines/types.ts +1 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/cli.ts +5 -35
- package/src/completion.ts +233 -41
- package/src/cycle/layout.ts +723 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +566 -0
- package/src/cycle/types.ts +98 -0
- package/src/d3.ts +107 -8
- package/src/dgmo-router.ts +82 -3
- package/src/echarts.ts +8 -5
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +17 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/state-parser.ts +5 -10
- package/src/index.ts +63 -2
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +32 -8
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/internal.ts +16 -0
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1521 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +31 -15
- package/src/mindmap/parser.ts +12 -18
- package/src/mindmap/renderer.ts +14 -13
- package/src/mindmap/text-wrap.ts +22 -12
- package/src/mindmap/types.ts +2 -2
- package/src/org/collapse.ts +81 -0
- package/src/org/parser.ts +2 -6
- package/src/org/renderer.ts +212 -4
- package/src/pyramid/parser.ts +172 -0
- package/src/pyramid/renderer.ts +684 -0
- package/src/pyramid/types.ts +28 -0
- package/src/render.ts +2 -8
- package/src/sequence/parser.ts +62 -20
- package/src/sequence/renderer.ts +146 -40
- package/src/sharing.ts +1 -0
- package/src/sitemap/layout.ts +21 -6
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1112 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/legend-layout.ts +3 -1
- package/src/utils/parsing.ts +47 -7
- package/src/utils/tag-groups.ts +46 -60
package/src/render.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { renderForExport } from './d3';
|
|
2
2
|
import { renderExtendedChartForExport } from './echarts';
|
|
3
|
-
import {
|
|
4
|
-
parseDgmoChartType,
|
|
5
|
-
getRenderCategory,
|
|
6
|
-
parseDgmo,
|
|
7
|
-
} from './dgmo-router';
|
|
3
|
+
import { getRenderCategory, parseDgmo } from './dgmo-router';
|
|
8
4
|
import type { DgmoError } from './diagnostics';
|
|
9
5
|
import { getPalette } from './palettes/registry';
|
|
10
6
|
import type { CompactViewState } from './sharing';
|
|
@@ -84,9 +80,7 @@ export async function render(
|
|
|
84
80
|
const paletteColors =
|
|
85
81
|
getPalette(paletteName)[theme === 'dark' ? 'dark' : 'light'];
|
|
86
82
|
|
|
87
|
-
const { diagnostics } = parseDgmo(content);
|
|
88
|
-
|
|
89
|
-
const chartType = parseDgmoChartType(content);
|
|
83
|
+
const { diagnostics, chartType } = parseDgmo(content);
|
|
90
84
|
const category = chartType ? getRenderCategory(chartType) : null;
|
|
91
85
|
|
|
92
86
|
// Build viewState from legendState (backwards compat) or use provided viewState
|
package/src/sequence/parser.ts
CHANGED
|
@@ -236,6 +236,8 @@ type NoteParseResult =
|
|
|
236
236
|
function parseNoteLine(
|
|
237
237
|
trimmed: string,
|
|
238
238
|
participants: SequenceParticipant[],
|
|
239
|
+
participantIds: Set<string>,
|
|
240
|
+
sortedParticipantsCache: SequenceParticipant[],
|
|
239
241
|
lastMsgFrom: string | null
|
|
240
242
|
): NoteParseResult {
|
|
241
243
|
const lower = trimmed.toLowerCase();
|
|
@@ -256,7 +258,7 @@ function parseNoteLine(
|
|
|
256
258
|
if (!lastMsgFrom) return { kind: 'skip' };
|
|
257
259
|
participantId = lastMsgFrom;
|
|
258
260
|
}
|
|
259
|
-
if (
|
|
261
|
+
if (participantIds.has(participantId)) {
|
|
260
262
|
return { kind: 'multi-head', position, participantId };
|
|
261
263
|
}
|
|
262
264
|
// Participant not found — fall through to bare-note handler for proper resolution
|
|
@@ -284,13 +286,17 @@ function parseNoteLine(
|
|
|
284
286
|
if (!afterPos) {
|
|
285
287
|
// Just `note left` or `note right` — multi-line head
|
|
286
288
|
if (!lastMsgFrom) return { kind: 'skip' };
|
|
287
|
-
if (!
|
|
288
|
-
return { kind: 'skip' };
|
|
289
|
+
if (!participantIds.has(lastMsgFrom)) return { kind: 'skip' };
|
|
289
290
|
return { kind: 'multi-head', position, participantId: lastMsgFrom };
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
// Try to match a known participant at the start of afterPos
|
|
293
|
-
const resolved = resolveParticipantAndText(
|
|
294
|
+
const resolved = resolveParticipantAndText(
|
|
295
|
+
afterPos,
|
|
296
|
+
participants,
|
|
297
|
+
participantIds,
|
|
298
|
+
sortedParticipantsCache
|
|
299
|
+
);
|
|
294
300
|
if (resolved) {
|
|
295
301
|
if (resolved.text) {
|
|
296
302
|
return {
|
|
@@ -316,8 +322,7 @@ function parseNoteLine(
|
|
|
316
322
|
|
|
317
323
|
// Without `of`, treat remaining text as note content on the last-msg sender
|
|
318
324
|
if (!lastMsgFrom) return { kind: 'skip' };
|
|
319
|
-
if (!
|
|
320
|
-
return { kind: 'skip' };
|
|
325
|
+
if (!participantIds.has(lastMsgFrom)) return { kind: 'skip' };
|
|
321
326
|
return {
|
|
322
327
|
kind: 'single',
|
|
323
328
|
position,
|
|
@@ -328,8 +333,7 @@ function parseNoteLine(
|
|
|
328
333
|
|
|
329
334
|
// Plain `note text` — default position, last msg sender
|
|
330
335
|
if (!lastMsgFrom) return { kind: 'skip' };
|
|
331
|
-
if (!
|
|
332
|
-
return { kind: 'skip' };
|
|
336
|
+
if (!participantIds.has(lastMsgFrom)) return { kind: 'skip' };
|
|
333
337
|
return {
|
|
334
338
|
kind: 'single',
|
|
335
339
|
position: 'right',
|
|
@@ -348,7 +352,9 @@ function parseNoteLine(
|
|
|
348
352
|
*/
|
|
349
353
|
function resolveParticipantAndText(
|
|
350
354
|
input: string,
|
|
351
|
-
participants: SequenceParticipant[]
|
|
355
|
+
participants: SequenceParticipant[],
|
|
356
|
+
participantIds: Set<string>,
|
|
357
|
+
sortedParticipantsCache: SequenceParticipant[]
|
|
352
358
|
): { participantId: string; text: string } | null {
|
|
353
359
|
// Handle quoted participant: `"Auth Service" text`
|
|
354
360
|
if (input.startsWith('"') || input.startsWith("'")) {
|
|
@@ -356,7 +362,7 @@ function resolveParticipantAndText(
|
|
|
356
362
|
const endQuote = input.indexOf(quote, 1);
|
|
357
363
|
if (endQuote > 0) {
|
|
358
364
|
const name = input.substring(1, endQuote);
|
|
359
|
-
if (
|
|
365
|
+
if (participantIds.has(name)) {
|
|
360
366
|
const text = input.substring(endQuote + 1).trim();
|
|
361
367
|
return { participantId: name, text };
|
|
362
368
|
}
|
|
@@ -364,8 +370,8 @@ function resolveParticipantAndText(
|
|
|
364
370
|
return null;
|
|
365
371
|
}
|
|
366
372
|
|
|
367
|
-
//
|
|
368
|
-
const sorted =
|
|
373
|
+
// Use pre-sorted participants (longest first) for greedy matching
|
|
374
|
+
const sorted = sortedParticipantsCache;
|
|
369
375
|
for (const p of sorted) {
|
|
370
376
|
if (input.startsWith(p.id)) {
|
|
371
377
|
const remaining = input.substring(p.id.length);
|
|
@@ -443,6 +449,25 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
443
449
|
// Group parsing state — tracks the active [Group] heading
|
|
444
450
|
let activeGroup: SequenceGroup | null = null;
|
|
445
451
|
|
|
452
|
+
// Fast lookup set for participant existence checks (mirrors result.participants)
|
|
453
|
+
const participantIds = new Set<string>();
|
|
454
|
+
|
|
455
|
+
// Cache sorted participants (longest ID first) for greedy name matching in notes.
|
|
456
|
+
// Invalidated whenever a new participant is added.
|
|
457
|
+
let sortedParticipantsCache: SequenceParticipant[] = [];
|
|
458
|
+
let sortedCacheDirty = true;
|
|
459
|
+
|
|
460
|
+
/** Get sorted participants, rebuilding cache only when dirty. */
|
|
461
|
+
const getSortedParticipants = (): SequenceParticipant[] => {
|
|
462
|
+
if (sortedCacheDirty) {
|
|
463
|
+
sortedParticipantsCache = [...result.participants].sort(
|
|
464
|
+
(a, b) => b.id.length - a.id.length
|
|
465
|
+
);
|
|
466
|
+
sortedCacheDirty = false;
|
|
467
|
+
}
|
|
468
|
+
return sortedParticipantsCache;
|
|
469
|
+
};
|
|
470
|
+
|
|
446
471
|
// Track participant → group name for duplicate membership detection
|
|
447
472
|
const participantGroupMap = new Map<string, string>();
|
|
448
473
|
|
|
@@ -774,7 +799,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
774
799
|
const position = posMatch ? parseInt(posMatch[1], 10) : undefined;
|
|
775
800
|
|
|
776
801
|
// Avoid duplicate participant declarations
|
|
777
|
-
if (!
|
|
802
|
+
if (!participantIds.has(id)) {
|
|
803
|
+
participantIds.add(id);
|
|
804
|
+
sortedCacheDirty = true;
|
|
778
805
|
result.participants.push({
|
|
779
806
|
id,
|
|
780
807
|
label: alias || id,
|
|
@@ -808,7 +835,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
808
835
|
const id = posOnlyMatch[1];
|
|
809
836
|
const position = parseInt(posOnlyMatch[2], 10);
|
|
810
837
|
|
|
811
|
-
if (!
|
|
838
|
+
if (!participantIds.has(id)) {
|
|
839
|
+
participantIds.add(id);
|
|
840
|
+
sortedCacheDirty = true;
|
|
812
841
|
result.participants.push({
|
|
813
842
|
id,
|
|
814
843
|
label: id,
|
|
@@ -846,7 +875,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
846
875
|
`'${id}(${color})' syntax is no longer supported — use 'tag:' groups for coloring`
|
|
847
876
|
);
|
|
848
877
|
contentStarted = true;
|
|
849
|
-
if (!
|
|
878
|
+
if (!participantIds.has(id)) {
|
|
879
|
+
participantIds.add(id);
|
|
880
|
+
sortedCacheDirty = true;
|
|
850
881
|
result.participants.push({
|
|
851
882
|
id,
|
|
852
883
|
label: id,
|
|
@@ -882,7 +913,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
882
913
|
) {
|
|
883
914
|
contentStarted = true;
|
|
884
915
|
const id = bareCore;
|
|
885
|
-
if (!
|
|
916
|
+
if (!participantIds.has(id)) {
|
|
917
|
+
participantIds.add(id);
|
|
886
918
|
result.participants.push({
|
|
887
919
|
id,
|
|
888
920
|
label: id,
|
|
@@ -965,7 +997,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
965
997
|
currentContainer().push(msg);
|
|
966
998
|
|
|
967
999
|
// Auto-register participants
|
|
968
|
-
if (!
|
|
1000
|
+
if (!participantIds.has(from)) {
|
|
1001
|
+
participantIds.add(from);
|
|
1002
|
+
sortedCacheDirty = true;
|
|
969
1003
|
result.participants.push({
|
|
970
1004
|
id: from,
|
|
971
1005
|
label: from,
|
|
@@ -973,7 +1007,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
973
1007
|
lineNumber,
|
|
974
1008
|
});
|
|
975
1009
|
}
|
|
976
|
-
if (!
|
|
1010
|
+
if (!participantIds.has(to)) {
|
|
1011
|
+
participantIds.add(to);
|
|
1012
|
+
sortedCacheDirty = true;
|
|
977
1013
|
result.participants.push({
|
|
978
1014
|
id: to,
|
|
979
1015
|
label: to,
|
|
@@ -1050,7 +1086,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1050
1086
|
result.messages.push(msg);
|
|
1051
1087
|
currentContainer().push(msg);
|
|
1052
1088
|
|
|
1053
|
-
if (!
|
|
1089
|
+
if (!participantIds.has(from)) {
|
|
1090
|
+
participantIds.add(from);
|
|
1091
|
+
sortedCacheDirty = true;
|
|
1054
1092
|
result.participants.push({
|
|
1055
1093
|
id: from,
|
|
1056
1094
|
label: from,
|
|
@@ -1058,7 +1096,9 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1058
1096
|
lineNumber,
|
|
1059
1097
|
});
|
|
1060
1098
|
}
|
|
1061
|
-
if (!
|
|
1099
|
+
if (!participantIds.has(to)) {
|
|
1100
|
+
participantIds.add(to);
|
|
1101
|
+
sortedCacheDirty = true;
|
|
1062
1102
|
result.participants.push({
|
|
1063
1103
|
id: to,
|
|
1064
1104
|
label: to,
|
|
@@ -1182,6 +1222,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1182
1222
|
const noteParsed = parseNoteLine(
|
|
1183
1223
|
trimmed,
|
|
1184
1224
|
result.participants,
|
|
1225
|
+
participantIds,
|
|
1226
|
+
getSortedParticipants(),
|
|
1185
1227
|
lastMsgFrom
|
|
1186
1228
|
);
|
|
1187
1229
|
if (noteParsed) {
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -28,7 +28,11 @@ import type { ResolvedTagMap } from './tag-resolution';
|
|
|
28
28
|
import { resolveActiveTagGroup } from '../utils/tag-groups';
|
|
29
29
|
import { LEGEND_HEIGHT } from '../utils/legend-constants';
|
|
30
30
|
import { renderLegendD3 } from '../utils/legend-d3';
|
|
31
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
LegendCallbacks,
|
|
33
|
+
LegendConfig,
|
|
34
|
+
LegendState,
|
|
35
|
+
} from '../utils/legend-types';
|
|
32
36
|
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
|
|
33
37
|
|
|
34
38
|
// ============================================================
|
|
@@ -73,14 +77,26 @@ function wrapTextLines(text: string, maxChars: number): string[] {
|
|
|
73
77
|
if (line.length <= maxChars) {
|
|
74
78
|
wrapped.push(line);
|
|
75
79
|
} else {
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
// Preserve bullet prefix: keep "- " glued to the first content word
|
|
81
|
+
// so wrapping never produces a bare "-" line.
|
|
82
|
+
const bulletPrefix = line.startsWith('- ') ? '- ' : '';
|
|
83
|
+
const content = bulletPrefix ? line.slice(2) : line;
|
|
84
|
+
const words = content.split(' ');
|
|
85
|
+
let current = bulletPrefix;
|
|
78
86
|
for (const word of words) {
|
|
79
|
-
|
|
87
|
+
const candidate = current ? current + ' ' + word : word;
|
|
88
|
+
if (
|
|
89
|
+
current &&
|
|
90
|
+
current !== bulletPrefix &&
|
|
91
|
+
candidate.length > maxChars
|
|
92
|
+
) {
|
|
80
93
|
wrapped.push(current);
|
|
81
94
|
current = word;
|
|
82
95
|
} else {
|
|
83
|
-
current =
|
|
96
|
+
current =
|
|
97
|
+
current && current !== bulletPrefix
|
|
98
|
+
? current + ' ' + word
|
|
99
|
+
: current + word;
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
if (current) wrapped.push(current);
|
|
@@ -544,6 +560,10 @@ export interface SequenceRenderOptions {
|
|
|
544
560
|
expandedNoteLines?: Set<number>; // keyed by note lineNumber; undefined = all expanded (CLI default)
|
|
545
561
|
exportWidth?: number; // Explicit width for CLI/export rendering (bypasses getBoundingClientRect)
|
|
546
562
|
activeTagGroup?: string | null; // Active tag group name for tag-driven recoloring; null = explicitly none
|
|
563
|
+
expandAllNotes?: boolean; // Whether the "Expand Notes" toggle is active
|
|
564
|
+
onExpandAllNotes?: (expand: boolean) => void; // Toggle all notes expanded/collapsed
|
|
565
|
+
controlsExpanded?: boolean; // Controls group expanded state (managed by React)
|
|
566
|
+
onToggleControlsExpand?: () => void; // Callback to toggle controls group
|
|
547
567
|
}
|
|
548
568
|
|
|
549
569
|
/**
|
|
@@ -1716,39 +1736,33 @@ export function renderSequenceDiagram(
|
|
|
1716
1736
|
}
|
|
1717
1737
|
}
|
|
1718
1738
|
|
|
1719
|
-
//
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
palette,
|
|
1747
|
-
isDark,
|
|
1748
|
-
undefined,
|
|
1749
|
-
svgWidth
|
|
1750
|
-
);
|
|
1751
|
-
}
|
|
1739
|
+
// Collect all note line numbers (for controls group visibility + "all expanded" check)
|
|
1740
|
+
const allNoteLineNumbers: number[] = [];
|
|
1741
|
+
const collectNoteLines = (els: SequenceElement[]): void => {
|
|
1742
|
+
for (const el of els) {
|
|
1743
|
+
if (isSequenceNote(el)) {
|
|
1744
|
+
allNoteLineNumbers.push(el.lineNumber);
|
|
1745
|
+
} else if (isSequenceBlock(el)) {
|
|
1746
|
+
collectNoteLines(el.children);
|
|
1747
|
+
if ('elseChildren' in el) collectNoteLines(el.elseChildren);
|
|
1748
|
+
if ('branches' in el && Array.isArray(el.branches)) {
|
|
1749
|
+
for (const branch of el.branches) {
|
|
1750
|
+
collectNoteLines(branch.children);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
collectNoteLines(elements);
|
|
1757
|
+
|
|
1758
|
+
// Show controls group only in interactive mode (expandedNoteLines defined)
|
|
1759
|
+
// when notes exist and collapse-notes is not disabled
|
|
1760
|
+
const showNotesControl =
|
|
1761
|
+
allNoteLineNumbers.length > 0 &&
|
|
1762
|
+
!collapseNotesDisabled &&
|
|
1763
|
+
expandedNoteLines !== undefined;
|
|
1764
|
+
|
|
1765
|
+
const hasTagGroups = parsed.tagGroups.length > 0;
|
|
1752
1766
|
|
|
1753
1767
|
// Build set of collapsed group names for drill-bar rendering
|
|
1754
1768
|
const collapsedGroupNames = new Set<string>();
|
|
@@ -2113,7 +2127,7 @@ export function renderSequenceDiagram(
|
|
|
2113
2127
|
firstBranchStep = Math.min(firstBranchStep, first);
|
|
2114
2128
|
}
|
|
2115
2129
|
if (firstBranchStep < Infinity) {
|
|
2116
|
-
const dividerY = stepY(firstBranchStep) -
|
|
2130
|
+
const dividerY = stepY(firstBranchStep) - BLOCK_HEADER_SPACE;
|
|
2117
2131
|
deferredLines.push({
|
|
2118
2132
|
x1: frameX,
|
|
2119
2133
|
y1: dividerY,
|
|
@@ -2142,7 +2156,7 @@ export function renderSequenceDiagram(
|
|
|
2142
2156
|
firstElseStep = Math.min(firstElseStep, first);
|
|
2143
2157
|
}
|
|
2144
2158
|
if (firstElseStep < Infinity) {
|
|
2145
|
-
const dividerY = stepY(firstElseStep) -
|
|
2159
|
+
const dividerY = stepY(firstElseStep) - BLOCK_HEADER_SPACE;
|
|
2146
2160
|
deferredLines.push({
|
|
2147
2161
|
x1: frameX,
|
|
2148
2162
|
y1: dividerY,
|
|
@@ -2805,6 +2819,73 @@ export function renderSequenceDiagram(
|
|
|
2805
2819
|
if (elements && elements.length > 0) {
|
|
2806
2820
|
renderNoteElements(elements);
|
|
2807
2821
|
}
|
|
2822
|
+
|
|
2823
|
+
// Render legend LAST so it sits on top of all other SVG elements
|
|
2824
|
+
// (group boxes, lifelines, participants, etc.) and can receive clicks.
|
|
2825
|
+
if (hasTagGroups || showNotesControl) {
|
|
2826
|
+
const controlsExpanded = options?.controlsExpanded ?? false;
|
|
2827
|
+
|
|
2828
|
+
const legendY = TOP_MARGIN + titleOffset;
|
|
2829
|
+
const resolvedGroups = parsed.tagGroups
|
|
2830
|
+
.filter((tg) => tg.entries.length > 0)
|
|
2831
|
+
.map((tg) => ({
|
|
2832
|
+
name: tg.name,
|
|
2833
|
+
entries: tg.entries.map((e) => ({
|
|
2834
|
+
value: e.value,
|
|
2835
|
+
color: e.color,
|
|
2836
|
+
})),
|
|
2837
|
+
}));
|
|
2838
|
+
|
|
2839
|
+
const allExpanded = showNotesControl && (options?.expandAllNotes ?? false);
|
|
2840
|
+
|
|
2841
|
+
const controlsGroup = showNotesControl
|
|
2842
|
+
? {
|
|
2843
|
+
toggles: [
|
|
2844
|
+
{
|
|
2845
|
+
id: 'expand-all-notes',
|
|
2846
|
+
type: 'toggle' as const,
|
|
2847
|
+
label: 'Expand Notes',
|
|
2848
|
+
active: allExpanded,
|
|
2849
|
+
onToggle: () => {},
|
|
2850
|
+
},
|
|
2851
|
+
],
|
|
2852
|
+
}
|
|
2853
|
+
: undefined;
|
|
2854
|
+
|
|
2855
|
+
const legendConfig: LegendConfig = {
|
|
2856
|
+
groups: resolvedGroups,
|
|
2857
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
2858
|
+
mode: 'fixed',
|
|
2859
|
+
controlsGroup,
|
|
2860
|
+
};
|
|
2861
|
+
const legendState: LegendState = {
|
|
2862
|
+
activeGroup: activeTagGroup ?? null,
|
|
2863
|
+
controlsExpanded,
|
|
2864
|
+
};
|
|
2865
|
+
|
|
2866
|
+
const legendCallbacks: LegendCallbacks = {
|
|
2867
|
+
onControlsExpand: () => {
|
|
2868
|
+
options?.onToggleControlsExpand?.();
|
|
2869
|
+
},
|
|
2870
|
+
onControlsToggle: (_toggleId: string, active: boolean) => {
|
|
2871
|
+
options?.onExpandAllNotes?.(active);
|
|
2872
|
+
},
|
|
2873
|
+
};
|
|
2874
|
+
|
|
2875
|
+
const legendG = svg
|
|
2876
|
+
.append('g')
|
|
2877
|
+
.attr('class', 'sequence-legend')
|
|
2878
|
+
.attr('transform', `translate(0,${legendY})`);
|
|
2879
|
+
renderLegendD3(
|
|
2880
|
+
legendG,
|
|
2881
|
+
legendConfig,
|
|
2882
|
+
legendState,
|
|
2883
|
+
palette,
|
|
2884
|
+
isDark,
|
|
2885
|
+
legendCallbacks,
|
|
2886
|
+
svgWidth
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2808
2889
|
}
|
|
2809
2890
|
|
|
2810
2891
|
/**
|
|
@@ -2843,6 +2924,31 @@ export function buildNoteMessageMap(
|
|
|
2843
2924
|
return map;
|
|
2844
2925
|
}
|
|
2845
2926
|
|
|
2927
|
+
/**
|
|
2928
|
+
* Collect all note line numbers from a sequence diagram's elements.
|
|
2929
|
+
* Used by the app to compute the "expand all" set.
|
|
2930
|
+
*/
|
|
2931
|
+
export function collectNoteLineNumbers(elements: SequenceElement[]): number[] {
|
|
2932
|
+
const result: number[] = [];
|
|
2933
|
+
const walk = (els: SequenceElement[]): void => {
|
|
2934
|
+
for (const el of els) {
|
|
2935
|
+
if (isSequenceNote(el)) {
|
|
2936
|
+
result.push(el.lineNumber);
|
|
2937
|
+
} else if (isSequenceBlock(el)) {
|
|
2938
|
+
walk(el.children);
|
|
2939
|
+
if (el.elseIfBranches) {
|
|
2940
|
+
for (const branch of el.elseIfBranches) {
|
|
2941
|
+
walk(branch.children);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
walk(el.elseChildren);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
walk(elements);
|
|
2949
|
+
return result;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2846
2952
|
function renderParticipant(
|
|
2847
2953
|
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
2848
2954
|
participant: SequenceParticipant,
|
package/src/sharing.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface CompactViewState {
|
|
|
32
32
|
io?: Record<string, number>; // instance overrides (infra)
|
|
33
33
|
hd?: boolean; // hide descriptions (mindmap)
|
|
34
34
|
cbd?: boolean; // color by depth (mindmap)
|
|
35
|
+
rq?: string; // radar quadrant focus (tech-radar position)
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export interface DecodedDiagramUrl {
|
package/src/sitemap/layout.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface SitemapLayoutNode {
|
|
|
22
22
|
metadata: Record<string, string>;
|
|
23
23
|
/** Original (unfiltered) metadata for tag-based coloring and hover dimming */
|
|
24
24
|
tagMetadata: Record<string, string>;
|
|
25
|
+
description?: string[];
|
|
25
26
|
isContainer: boolean;
|
|
26
27
|
lineNumber: number;
|
|
27
28
|
color?: string;
|
|
@@ -161,23 +162,36 @@ function filterMetadata(
|
|
|
161
162
|
return filtered;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
function computeCardWidth(
|
|
165
|
+
function computeCardWidth(
|
|
166
|
+
label: string,
|
|
167
|
+
meta: Record<string, string>,
|
|
168
|
+
descLines?: string[]
|
|
169
|
+
): number {
|
|
165
170
|
let maxChars = label.length;
|
|
166
171
|
for (const [key, value] of Object.entries(meta)) {
|
|
167
172
|
const lineChars = key.length + 2 + value.length;
|
|
168
173
|
if (lineChars > maxChars) maxChars = lineChars;
|
|
169
174
|
}
|
|
175
|
+
if (descLines) {
|
|
176
|
+
for (const dl of descLines) {
|
|
177
|
+
if (dl.length > maxChars) maxChars = dl.length;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
170
180
|
return Math.max(
|
|
171
181
|
MIN_CARD_WIDTH,
|
|
172
182
|
Math.ceil(maxChars * CHAR_WIDTH) + CARD_H_PAD * 2
|
|
173
183
|
);
|
|
174
184
|
}
|
|
175
185
|
|
|
176
|
-
function computeCardHeight(
|
|
186
|
+
function computeCardHeight(
|
|
187
|
+
meta: Record<string, string>,
|
|
188
|
+
descLineCount = 0
|
|
189
|
+
): number {
|
|
177
190
|
const metaCount = Object.keys(meta).length;
|
|
178
|
-
|
|
191
|
+
const contentCount = metaCount + descLineCount;
|
|
192
|
+
if (contentCount === 0) return HEADER_HEIGHT + CARD_V_PAD;
|
|
179
193
|
return (
|
|
180
|
-
HEADER_HEIGHT + SEPARATOR_GAP +
|
|
194
|
+
HEADER_HEIGHT + SEPARATOR_GAP + contentCount * META_LINE_HEIGHT + CARD_V_PAD
|
|
181
195
|
);
|
|
182
196
|
}
|
|
183
197
|
|
|
@@ -307,8 +321,8 @@ function flattenNodes(
|
|
|
307
321
|
parentPageId,
|
|
308
322
|
meta,
|
|
309
323
|
fullMeta: { ...node.metadata },
|
|
310
|
-
width: computeCardWidth(node.label, meta),
|
|
311
|
-
height: computeCardHeight(meta),
|
|
324
|
+
width: computeCardWidth(node.label, meta, node.description),
|
|
325
|
+
height: computeCardHeight(meta, node.description?.length ?? 0),
|
|
312
326
|
});
|
|
313
327
|
// Pages can have children too (nested pages) — this page becomes the parentPageId
|
|
314
328
|
if (node.children.length > 0) {
|
|
@@ -519,6 +533,7 @@ export function layoutSitemap(
|
|
|
519
533
|
label: node.label,
|
|
520
534
|
metadata: flat.meta,
|
|
521
535
|
tagMetadata: flat.fullMeta,
|
|
536
|
+
description: node.description,
|
|
522
537
|
isContainer: false,
|
|
523
538
|
lineNumber: node.lineNumber,
|
|
524
539
|
color: resolveNodeColor(node, parsed.tagGroups, activeTagGroup ?? null),
|
package/src/sitemap/parser.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
ALL_CHART_TYPES,
|
|
26
26
|
} from '../utils/parsing';
|
|
27
27
|
import type { SitemapNode, ParsedSitemap } from './types';
|
|
28
|
+
import { tryStripDescriptionKeyword } from '../utils/description-helpers';
|
|
28
29
|
|
|
29
30
|
// ============================================================
|
|
30
31
|
// Regexes
|
|
@@ -370,13 +371,7 @@ export function parseSitemap(
|
|
|
370
371
|
: trimmed.match(METADATA_RE);
|
|
371
372
|
|
|
372
373
|
if (containerMatch) {
|
|
373
|
-
const
|
|
374
|
-
const { label, color } = extractColor(
|
|
375
|
-
rawLabel,
|
|
376
|
-
palette,
|
|
377
|
-
result.diagnostics,
|
|
378
|
-
lineNumber
|
|
379
|
-
);
|
|
374
|
+
const label = containerMatch[1].trim();
|
|
380
375
|
|
|
381
376
|
// Parse optional pipe metadata on the container line
|
|
382
377
|
const pipeStr = containerMatch[2];
|
|
@@ -399,7 +394,6 @@ export function parseSitemap(
|
|
|
399
394
|
parentId: null,
|
|
400
395
|
isContainer: true,
|
|
401
396
|
lineNumber,
|
|
402
|
-
color,
|
|
403
397
|
};
|
|
404
398
|
|
|
405
399
|
attachNode(node, indent, indentStack, result);
|
|
@@ -435,6 +429,17 @@ export function parseSitemap(
|
|
|
435
429
|
pushError(lineNumber, 'Metadata has no parent node');
|
|
436
430
|
}
|
|
437
431
|
} else {
|
|
432
|
+
// Check if this is a description line for a parent node
|
|
433
|
+
const descResult = tryStripDescriptionKeyword(trimmed);
|
|
434
|
+
if (descResult.isKeyword && indentStack.length > 0) {
|
|
435
|
+
const parent = findParentNode(indent, indentStack);
|
|
436
|
+
if (parent) {
|
|
437
|
+
if (!parent.description) parent.description = [];
|
|
438
|
+
parent.description.push(descResult.text.trim());
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
438
443
|
// Node label — possibly with pipe-delimited metadata
|
|
439
444
|
const node = parseNodeLabel(
|
|
440
445
|
trimmed,
|
|
@@ -531,31 +536,35 @@ function parseNodeLabel(
|
|
|
531
536
|
counter: number,
|
|
532
537
|
aliasMap: Map<string, string> = new Map(),
|
|
533
538
|
warnFn?: (line: number, msg: string) => void,
|
|
534
|
-
|
|
539
|
+
_diagnostics?: DgmoError[]
|
|
535
540
|
): SitemapNode {
|
|
536
541
|
const segments = trimmed.split('|').map((s) => s.trim());
|
|
537
|
-
const
|
|
538
|
-
const { label, color } = extractColor(
|
|
539
|
-
rawLabel,
|
|
540
|
-
palette,
|
|
541
|
-
diagnostics,
|
|
542
|
-
lineNumber
|
|
543
|
-
);
|
|
542
|
+
const label = segments[0];
|
|
544
543
|
const metadata = parsePipeMetadata(
|
|
545
544
|
segments,
|
|
546
545
|
aliasMap,
|
|
547
546
|
warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined
|
|
548
547
|
);
|
|
549
548
|
|
|
549
|
+
// Extract description from pipe metadata into dedicated field
|
|
550
|
+
let description: string[] | undefined;
|
|
551
|
+
if ('description' in metadata) {
|
|
552
|
+
const descVal = metadata['description'].trim();
|
|
553
|
+
if (descVal) {
|
|
554
|
+
description = [descVal];
|
|
555
|
+
}
|
|
556
|
+
delete metadata['description'];
|
|
557
|
+
}
|
|
558
|
+
|
|
550
559
|
return {
|
|
551
560
|
id: `node-${counter}`,
|
|
552
561
|
label,
|
|
553
562
|
metadata,
|
|
563
|
+
description,
|
|
554
564
|
children: [],
|
|
555
565
|
parentId: null,
|
|
556
566
|
isContainer: false,
|
|
557
567
|
lineNumber,
|
|
558
|
-
color,
|
|
559
568
|
};
|
|
560
569
|
}
|
|
561
570
|
|