@diagrammo/dgmo 0.6.2 → 0.7.0
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/.claude/commands/dgmo.md +231 -13
- package/AGENTS.md +148 -0
- package/dist/cli.cjs +341 -165
- package/dist/index.cjs +4900 -1685
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +259 -18
- package/dist/index.d.ts +259 -18
- package/dist/index.js +4642 -1436
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/c4/layout.ts +0 -5
- package/src/c4/parser.ts +0 -16
- package/src/c4/renderer.ts +7 -11
- package/src/class/layout.ts +0 -1
- package/src/class/parser.ts +28 -0
- package/src/class/renderer.ts +189 -34
- package/src/cli.ts +566 -25
- package/src/colors.ts +3 -3
- package/src/completion.ts +58 -0
- package/src/d3.ts +179 -122
- package/src/dgmo-router.ts +3 -58
- package/src/echarts.ts +96 -55
- package/src/er/parser.ts +30 -1
- package/src/er/renderer.ts +12 -7
- package/src/gantt/calculator.ts +677 -0
- package/src/gantt/parser.ts +761 -0
- package/src/gantt/renderer.ts +2125 -0
- package/src/gantt/resolver.ts +144 -0
- package/src/gantt/types.ts +168 -0
- package/src/graph/flowchart-parser.ts +27 -4
- package/src/graph/flowchart-renderer.ts +1 -2
- package/src/graph/state-parser.ts +0 -1
- package/src/graph/state-renderer.ts +1 -3
- package/src/index.ts +37 -0
- package/src/infra/compute.ts +0 -7
- package/src/infra/layout.ts +0 -2
- package/src/infra/parser.ts +46 -4
- package/src/infra/renderer.ts +49 -27
- package/src/initiative-status/filter.ts +63 -0
- package/src/initiative-status/layout.ts +319 -67
- package/src/initiative-status/parser.ts +200 -25
- package/src/initiative-status/renderer.ts +298 -35
- package/src/initiative-status/types.ts +6 -0
- package/src/kanban/parser.ts +0 -2
- package/src/org/layout.ts +22 -59
- package/src/org/renderer.ts +11 -36
- package/src/palettes/dracula.ts +60 -0
- package/src/palettes/index.ts +8 -6
- package/src/palettes/monokai.ts +60 -0
- package/src/palettes/registry.ts +4 -2
- package/src/sequence/parser.ts +14 -11
- package/src/sequence/renderer.ts +5 -6
- package/src/sequence/tag-resolution.ts +0 -1
- package/src/sharing.ts +8 -0
- package/src/sitemap/layout.ts +1 -14
- package/src/sitemap/parser.ts +1 -2
- package/src/sitemap/renderer.ts +4 -7
- package/src/utils/arrows.ts +7 -7
- package/src/utils/duration.ts +212 -0
- package/src/utils/export-container.ts +40 -0
- package/src/utils/legend-constants.ts +1 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PaletteConfig } from './types';
|
|
2
|
+
import { registerPalette } from './registry';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Dracula Palette Definition
|
|
6
|
+
// https://draculatheme.com/contribute
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
export const draculaPalette: PaletteConfig = {
|
|
10
|
+
id: 'dracula',
|
|
11
|
+
name: 'Dracula',
|
|
12
|
+
light: {
|
|
13
|
+
bg: '#f8f8f2', // foreground as light bg
|
|
14
|
+
surface: '#f0f0ec',
|
|
15
|
+
overlay: '#e8e8e2',
|
|
16
|
+
border: '#d8d8d2',
|
|
17
|
+
text: '#282a36', // background as light text
|
|
18
|
+
textMuted: '#44475a', // current line
|
|
19
|
+
primary: '#6272a4', // comment
|
|
20
|
+
secondary: '#bd93f9', // purple
|
|
21
|
+
accent: '#bd93f9', // purple
|
|
22
|
+
destructive: '#ff5555', // red
|
|
23
|
+
colors: {
|
|
24
|
+
red: '#ff5555',
|
|
25
|
+
orange: '#ffb86c',
|
|
26
|
+
yellow: '#f1fa8c',
|
|
27
|
+
green: '#50fa7b',
|
|
28
|
+
blue: '#6272a4', // comment blue
|
|
29
|
+
purple: '#bd93f9',
|
|
30
|
+
teal: '#5ac8b8', // muted cyan toward green
|
|
31
|
+
cyan: '#8be9fd',
|
|
32
|
+
gray: '#6272a4',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
dark: {
|
|
36
|
+
bg: '#282a36', // background
|
|
37
|
+
surface: '#343746', // between bg and current line
|
|
38
|
+
overlay: '#44475a', // current line
|
|
39
|
+
border: '#6272a4', // comment
|
|
40
|
+
text: '#f8f8f2', // foreground
|
|
41
|
+
textMuted: '#bcc2d4', // muted foreground
|
|
42
|
+
primary: '#bd93f9', // purple (Dracula's signature)
|
|
43
|
+
secondary: '#8be9fd', // cyan
|
|
44
|
+
accent: '#ff79c6', // pink
|
|
45
|
+
destructive: '#ff5555', // red
|
|
46
|
+
colors: {
|
|
47
|
+
red: '#ff5555',
|
|
48
|
+
orange: '#ffb86c',
|
|
49
|
+
yellow: '#f1fa8c',
|
|
50
|
+
green: '#50fa7b',
|
|
51
|
+
blue: '#6272a4', // comment blue
|
|
52
|
+
purple: '#bd93f9',
|
|
53
|
+
teal: '#5ac8b8', // muted cyan toward green
|
|
54
|
+
cyan: '#8be9fd',
|
|
55
|
+
gray: '#6272a4',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
registerPalette(draculaPalette);
|
package/src/palettes/index.ts
CHANGED
|
@@ -22,15 +22,17 @@ export {
|
|
|
22
22
|
contrastText,
|
|
23
23
|
} from './color-utils';
|
|
24
24
|
|
|
25
|
-
// Re-export palette definitions
|
|
26
|
-
export {
|
|
27
|
-
export { solarizedPalette } from './solarized';
|
|
25
|
+
// Re-export palette definitions (alphabetical)
|
|
26
|
+
export { boldPalette } from './bold';
|
|
28
27
|
export { catppuccinPalette } from './catppuccin';
|
|
29
|
-
export {
|
|
28
|
+
export { draculaPalette } from './dracula';
|
|
30
29
|
export { gruvboxPalette } from './gruvbox';
|
|
31
|
-
export {
|
|
30
|
+
export { monokaiPalette } from './monokai';
|
|
31
|
+
export { nordPalette } from './nord';
|
|
32
32
|
export { oneDarkPalette } from './one-dark';
|
|
33
|
-
export {
|
|
33
|
+
export { rosePinePalette } from './rose-pine';
|
|
34
|
+
export { solarizedPalette } from './solarized';
|
|
35
|
+
export { tokyoNightPalette } from './tokyo-night';
|
|
34
36
|
|
|
35
37
|
// Re-export Mermaid bridge
|
|
36
38
|
export { buildMermaidThemeVars, buildThemeCSS } from './mermaid-bridge';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PaletteConfig } from './types';
|
|
2
|
+
import { registerPalette } from './registry';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Monokai Palette Definition
|
|
6
|
+
// Based on Monokai / Monokai Pro color scheme
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
export const monokaiPalette: PaletteConfig = {
|
|
10
|
+
id: 'monokai',
|
|
11
|
+
name: 'Monokai',
|
|
12
|
+
light: {
|
|
13
|
+
bg: '#fafaf8',
|
|
14
|
+
surface: '#f0efe8',
|
|
15
|
+
overlay: '#e6e5de',
|
|
16
|
+
border: '#d4d3cc',
|
|
17
|
+
text: '#272822', // classic Monokai bg as text
|
|
18
|
+
textMuted: '#75715e', // comment
|
|
19
|
+
primary: '#49483e', // line highlight
|
|
20
|
+
secondary: '#f92672', // pink
|
|
21
|
+
accent: '#a6e22e', // green
|
|
22
|
+
destructive: '#f92672', // pink/red
|
|
23
|
+
colors: {
|
|
24
|
+
red: '#f92672', // Monokai pink-red
|
|
25
|
+
orange: '#fd971f',
|
|
26
|
+
yellow: '#e6db74',
|
|
27
|
+
green: '#a6e22e',
|
|
28
|
+
blue: '#5c7eab', // derived true blue
|
|
29
|
+
purple: '#ae81ff',
|
|
30
|
+
teal: '#4ea8a6', // muted from cyan
|
|
31
|
+
cyan: '#66d9ef',
|
|
32
|
+
gray: '#75715e', // comment
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
dark: {
|
|
36
|
+
bg: '#272822', // classic background
|
|
37
|
+
surface: '#2d2e27',
|
|
38
|
+
overlay: '#3e3d32', // line highlight
|
|
39
|
+
border: '#49483e',
|
|
40
|
+
text: '#f8f8f2', // foreground
|
|
41
|
+
textMuted: '#a6a28c', // brightened comment
|
|
42
|
+
primary: '#a6e22e', // green
|
|
43
|
+
secondary: '#66d9ef', // cyan
|
|
44
|
+
accent: '#f92672', // pink
|
|
45
|
+
destructive: '#f92672', // pink/red
|
|
46
|
+
colors: {
|
|
47
|
+
red: '#f92672',
|
|
48
|
+
orange: '#fd971f',
|
|
49
|
+
yellow: '#e6db74',
|
|
50
|
+
green: '#a6e22e',
|
|
51
|
+
blue: '#5c7eab', // derived true blue
|
|
52
|
+
purple: '#ae81ff',
|
|
53
|
+
teal: '#4ea8a6', // muted from cyan
|
|
54
|
+
cyan: '#66d9ef',
|
|
55
|
+
gray: '#75715e', // comment
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
registerPalette(monokaiPalette);
|
package/src/palettes/registry.ts
CHANGED
|
@@ -86,7 +86,9 @@ export function getPalette(id: string): PaletteConfig {
|
|
|
86
86
|
return PALETTE_REGISTRY.get(id) ?? PALETTE_REGISTRY.get(DEFAULT_PALETTE_ID)!;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** List all registered palettes (for the selector UI). */
|
|
89
|
+
/** List all registered palettes alphabetically (for the selector UI). */
|
|
90
90
|
export function getAvailablePalettes(): PaletteConfig[] {
|
|
91
|
-
return Array.from(PALETTE_REGISTRY.values())
|
|
91
|
+
return Array.from(PALETTE_REGISTRY.values()).sort((a, b) =>
|
|
92
|
+
a.name.localeCompare(b.name)
|
|
93
|
+
);
|
|
92
94
|
}
|
package/src/sequence/parser.ts
CHANGED
|
@@ -153,12 +153,14 @@ export interface ParsedSequenceDgmo {
|
|
|
153
153
|
error: string | null;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// "Name is a type" pattern — e.g. "
|
|
156
|
+
// "Name is a type" pattern — e.g. "Auth Server is a service"
|
|
157
|
+
// Participant names may contain spaces; [^:]+? stops at colons so that
|
|
158
|
+
// note lines like "note right of A: this is a service" are not falsely matched.
|
|
157
159
|
// Remainder after type is parsed separately for aka/position modifiers
|
|
158
|
-
const IS_A_PATTERN = /^(
|
|
160
|
+
const IS_A_PATTERN = /^([^:]+?)\s+is\s+an?\s+(\w+)(?:\s+(.+))?$/i;
|
|
159
161
|
|
|
160
162
|
// Standalone "Name position N" pattern — e.g. "DB position -1"
|
|
161
|
-
const POSITION_ONLY_PATTERN = /^(
|
|
163
|
+
const POSITION_ONLY_PATTERN = /^([^:]+?)\s+position\s+(-?\d+)$/i;
|
|
162
164
|
|
|
163
165
|
// Colored participant declaration — e.g. "Tapin2(green)", "API(blue)"
|
|
164
166
|
const COLORED_PARTICIPANT_PATTERN = /^(\S+?)\(([^)]+)\)\s*$/;
|
|
@@ -174,9 +176,10 @@ const SECTION_PATTERN = /^==\s+(.+?)(?:\s*==)?\s*$/;
|
|
|
174
176
|
// Arrow pattern for sequence inference — detects any arrow form
|
|
175
177
|
const ARROW_PATTERN = /\S+\s*(?:<-\S+-|<~\S+~|-\S+->|~\S+~>|->|~>|<-|<~)\s*\S+/;
|
|
176
178
|
|
|
177
|
-
// Note patterns — "note: text", "note right of
|
|
178
|
-
|
|
179
|
-
const
|
|
179
|
+
// Note patterns — "note: text", "note right of Auth Server: text"
|
|
180
|
+
// Participant names may contain spaces; the colon acts as the delimiter.
|
|
181
|
+
const NOTE_SINGLE = /^note(?:\s+(right|left)\s+of\s+(.+?))?\s*:\s*(.+)$/i;
|
|
182
|
+
const NOTE_MULTI = /^note(?:\s+(right|left)\s+of\s+(.+?))?\s*:?\s*$/i;
|
|
180
183
|
|
|
181
184
|
/**
|
|
182
185
|
* Parse a .dgmo file with `chart: sequence` into a structured representation.
|
|
@@ -673,7 +676,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
673
676
|
|
|
674
677
|
// ---- Error: plain bidirectional arrows (A <-> B, A <~> B) ----
|
|
675
678
|
const bidiPlainMatch = arrowCore.match(
|
|
676
|
-
/^(
|
|
679
|
+
/^(.+?)\s*(?:<->|<~>)\s*(.+)/
|
|
677
680
|
);
|
|
678
681
|
if (bidiPlainMatch) {
|
|
679
682
|
pushError(
|
|
@@ -684,8 +687,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
684
687
|
}
|
|
685
688
|
|
|
686
689
|
// ---- Deprecated bare return arrows: A <- B, A <~ B ----
|
|
687
|
-
const bareReturnSync = arrowCore.match(/^(
|
|
688
|
-
const bareReturnAsync = arrowCore.match(/^(
|
|
690
|
+
const bareReturnSync = arrowCore.match(/^(.+?)\s+<-\s+(.+)$/);
|
|
691
|
+
const bareReturnAsync = arrowCore.match(/^(.+?)\s+<~\s+(.+)$/);
|
|
689
692
|
const bareReturn = bareReturnSync || bareReturnAsync;
|
|
690
693
|
if (bareReturn) {
|
|
691
694
|
const to = bareReturn[1];
|
|
@@ -698,8 +701,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
698
701
|
}
|
|
699
702
|
|
|
700
703
|
// ---- Bare (unlabeled) call arrows: A -> B, A ~> B ----
|
|
701
|
-
const bareCallSync = arrowCore.match(/^(
|
|
702
|
-
const bareCallAsync = arrowCore.match(/^(
|
|
704
|
+
const bareCallSync = arrowCore.match(/^(.+?)\s*->\s*(.+)$/);
|
|
705
|
+
const bareCallAsync = arrowCore.match(/^(.+?)\s*~>\s*(.+)$/);
|
|
703
706
|
const bareCall = bareCallSync || bareCallAsync;
|
|
704
707
|
if (bareCall) {
|
|
705
708
|
contentStarted = true;
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
truncateBareUrl,
|
|
11
11
|
renderInlineText,
|
|
12
12
|
} from '../utils/inline-markdown';
|
|
13
|
-
export type { InlineSpan } from '../utils/inline-markdown';
|
|
14
13
|
export { parseInlineMarkdown, truncateBareUrl };
|
|
15
14
|
import { FONT_FAMILY } from '../fonts';
|
|
16
15
|
import { resolveColor } from '../colors';
|
|
@@ -1282,10 +1281,12 @@ export function renderSequenceDiagram(
|
|
|
1282
1281
|
|
|
1283
1282
|
// Compute cumulative Y positions for each step, with section dividers as stable anchors
|
|
1284
1283
|
const titleOffset = title ? TITLE_HEIGHT : 0;
|
|
1284
|
+
const LEGEND_FIXED_GAP = 8;
|
|
1285
|
+
const legendTopSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
1285
1286
|
const groupOffset =
|
|
1286
1287
|
groups.length > 0 ? GROUP_PADDING_TOP + GROUP_LABEL_SIZE : 0;
|
|
1287
1288
|
const participantStartY =
|
|
1288
|
-
TOP_MARGIN + titleOffset + PARTICIPANT_Y_OFFSET + groupOffset;
|
|
1289
|
+
TOP_MARGIN + titleOffset + legendTopSpace + PARTICIPANT_Y_OFFSET + groupOffset;
|
|
1289
1290
|
const lifelineStartY0 = participantStartY + PARTICIPANT_BOX_HEIGHT;
|
|
1290
1291
|
const hasActors = participants.some((p) => p.type === 'actor');
|
|
1291
1292
|
const messageStartOffset = MESSAGE_START_OFFSET + (hasActors ? 20 : 0);
|
|
@@ -1391,8 +1392,7 @@ export function renderSequenceDiagram(
|
|
|
1391
1392
|
PARTICIPANT_BOX_HEIGHT +
|
|
1392
1393
|
Math.max(lifelineLength, 40) +
|
|
1393
1394
|
40;
|
|
1394
|
-
const
|
|
1395
|
-
const totalHeight = contentHeight + legendSpace;
|
|
1395
|
+
const totalHeight = contentHeight;
|
|
1396
1396
|
|
|
1397
1397
|
const containerWidth = options?.exportWidth ?? container.getBoundingClientRect().width;
|
|
1398
1398
|
const svgWidth = Math.max(totalWidth, containerWidth);
|
|
@@ -1571,7 +1571,7 @@ export function renderSequenceDiagram(
|
|
|
1571
1571
|
|
|
1572
1572
|
// Render legend pills for tag groups
|
|
1573
1573
|
if (parsed.tagGroups.length > 0) {
|
|
1574
|
-
const legendY =
|
|
1574
|
+
const legendY = TOP_MARGIN + titleOffset;
|
|
1575
1575
|
const groupBg = isDark
|
|
1576
1576
|
? mix(palette.surface, palette.bg, 50)
|
|
1577
1577
|
: mix(palette.surface, palette.bg, 30);
|
|
@@ -2129,7 +2129,6 @@ export function renderSequenceDiagram(
|
|
|
2129
2129
|
// IMPORTANT: only the <g> carries data-line-number / data-section —
|
|
2130
2130
|
// children must NOT have them, otherwise the click walk-up resolves
|
|
2131
2131
|
// to a line-number navigation before reaching data-section-toggle.
|
|
2132
|
-
const HIT_AREA_HEIGHT = 36;
|
|
2133
2132
|
const sectionG = svg
|
|
2134
2133
|
.append('g')
|
|
2135
2134
|
.attr('data-section-toggle', '')
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// Resolves effective tag values for participants and messages
|
|
6
6
|
// using the priority chain: explicit > group > receiver-inherit > default > neutral
|
|
7
7
|
|
|
8
|
-
import type { TagGroup } from '../utils/tag-groups';
|
|
9
8
|
import type {
|
|
10
9
|
ParsedSequenceDgmo,
|
|
11
10
|
SequenceParticipant,
|
package/src/sharing.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface DiagramViewState {
|
|
|
10
10
|
activeTagGroup?: string;
|
|
11
11
|
collapsedGroups?: string[];
|
|
12
12
|
swimlaneTagGroup?: string;
|
|
13
|
+
collapsedLanes?: string[];
|
|
13
14
|
palette?: string;
|
|
14
15
|
theme?: 'light' | 'dark';
|
|
15
16
|
}
|
|
@@ -59,6 +60,10 @@ export function encodeDiagramUrl(
|
|
|
59
60
|
hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
if (options?.viewState?.collapsedLanes?.length) {
|
|
64
|
+
hash += `&cl=${encodeURIComponent(options.viewState.collapsedLanes.join(','))}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
if (options?.viewState?.palette && options.viewState.palette !== 'nord') {
|
|
63
68
|
hash += `&pal=${encodeURIComponent(options.viewState.palette)}`;
|
|
64
69
|
}
|
|
@@ -115,6 +120,9 @@ export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
|
|
|
115
120
|
if (key === 'swim' && val) {
|
|
116
121
|
viewState.swimlaneTagGroup = val;
|
|
117
122
|
}
|
|
123
|
+
if (key === 'cl' && val) {
|
|
124
|
+
viewState.collapsedLanes = val.split(',').filter(Boolean);
|
|
125
|
+
}
|
|
118
126
|
if (key === 'pal' && val) viewState.palette = val;
|
|
119
127
|
if (key === 'th' && (val === 'light' || val === 'dark')) viewState.theme = val;
|
|
120
128
|
}
|
package/src/sitemap/layout.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import dagre from '@dagrejs/dagre';
|
|
6
|
-
import type { ParsedSitemap, SitemapNode
|
|
6
|
+
import type { ParsedSitemap, SitemapNode } from './types';
|
|
7
7
|
import type { TagGroup } from '../utils/tag-groups';
|
|
8
8
|
import { resolveTagColor, injectDefaultTagMetadata } from '../utils/tag-groups';
|
|
9
9
|
|
|
@@ -89,8 +89,6 @@ export interface SitemapLayoutResult {
|
|
|
89
89
|
// ============================================================
|
|
90
90
|
|
|
91
91
|
const CHAR_WIDTH = 7.5;
|
|
92
|
-
const LABEL_FONT_SIZE = 13;
|
|
93
|
-
const META_FONT_SIZE = 11;
|
|
94
92
|
const META_LINE_HEIGHT = 16;
|
|
95
93
|
const HEADER_HEIGHT = 28;
|
|
96
94
|
const SEPARATOR_GAP = 6;
|
|
@@ -105,7 +103,6 @@ const CONTAINER_LABEL_HEIGHT = 28;
|
|
|
105
103
|
const CONTAINER_META_LINE_HEIGHT = 16;
|
|
106
104
|
|
|
107
105
|
// Legend (kanban-style pills)
|
|
108
|
-
const LEGEND_GAP = 30;
|
|
109
106
|
const LEGEND_HEIGHT = 28;
|
|
110
107
|
const LEGEND_PILL_PAD = 16;
|
|
111
108
|
const LEGEND_PILL_FONT_W = 11 * 0.6;
|
|
@@ -162,16 +159,6 @@ function resolveNodeColor(
|
|
|
162
159
|
|
|
163
160
|
const OVERLAP_GAP = 20;
|
|
164
161
|
|
|
165
|
-
function countDescendantNodes(node: SitemapNode, hiddenCounts?: Map<string, number>): number {
|
|
166
|
-
let count = 0;
|
|
167
|
-
for (const child of node.children) {
|
|
168
|
-
count += (child.isContainer ? 0 : 1) + countDescendantNodes(child, hiddenCounts);
|
|
169
|
-
const hc = hiddenCounts?.get(child.id);
|
|
170
|
-
if (hc) count += hc;
|
|
171
|
-
}
|
|
172
|
-
return count;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
162
|
// ============================================================
|
|
176
163
|
// Legend
|
|
177
164
|
// ============================================================
|
package/src/sitemap/parser.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { PaletteColors } from '../palettes';
|
|
6
6
|
import { resolveColor } from '../colors';
|
|
7
7
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
8
|
-
import type { TagGroup
|
|
8
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
9
9
|
import { isTagBlockHeading, matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
|
|
10
10
|
import {
|
|
11
11
|
measureIndent,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
} from '../utils/parsing';
|
|
18
18
|
import type {
|
|
19
19
|
SitemapNode,
|
|
20
|
-
SitemapEdge,
|
|
21
20
|
SitemapDirection,
|
|
22
21
|
ParsedSitemap,
|
|
23
22
|
} from './types';
|
package/src/sitemap/renderer.ts
CHANGED
|
@@ -10,9 +10,6 @@ import { mix } from '../palettes/color-utils';
|
|
|
10
10
|
import type { ParsedSitemap } from './types';
|
|
11
11
|
import type {
|
|
12
12
|
SitemapLayoutResult,
|
|
13
|
-
SitemapLayoutNode,
|
|
14
|
-
SitemapLayoutEdge,
|
|
15
|
-
SitemapContainerBounds,
|
|
16
13
|
SitemapLegendGroup,
|
|
17
14
|
} from './layout';
|
|
18
15
|
import {
|
|
@@ -130,9 +127,9 @@ export function renderSitemap(
|
|
|
130
127
|
const fixedTitle = fixedLegend && !!parsed.title;
|
|
131
128
|
const fixedTitleH = fixedTitle ? TITLE_HEIGHT : 0;
|
|
132
129
|
const legendReserveH = fixedLegend ? LEGEND_HEIGHT + LEGEND_FIXED_GAP : 0;
|
|
133
|
-
// Space reserved above content (title
|
|
134
|
-
const fixedReserveTop = fixedTitleH;
|
|
135
|
-
const fixedReserveBottom =
|
|
130
|
+
// Space reserved above content (title + legend)
|
|
131
|
+
const fixedReserveTop = fixedTitleH + legendReserveH;
|
|
132
|
+
const fixedReserveBottom = 0;
|
|
136
133
|
// Title inside scaled group only when legend is NOT fixed
|
|
137
134
|
const titleOffset = !fixedTitle && parsed.title ? TITLE_HEIGHT : 0;
|
|
138
135
|
|
|
@@ -546,7 +543,7 @@ export function renderSitemap(
|
|
|
546
543
|
const legendParent = svg
|
|
547
544
|
.append('g')
|
|
548
545
|
.attr('class', 'sitemap-legend-fixed')
|
|
549
|
-
.attr('transform', `translate(0, ${
|
|
546
|
+
.attr('transform', `translate(0, ${DIAGRAM_PADDING + fixedTitleH})`);
|
|
550
547
|
if (activeTagGroup) {
|
|
551
548
|
legendParent.attr('data-legend-active', activeTagGroup.toLowerCase());
|
|
552
549
|
}
|
package/src/utils/arrows.ts
CHANGED
|
@@ -13,15 +13,15 @@ export interface ParsedArrow {
|
|
|
13
13
|
async: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// Forward (call) patterns
|
|
17
|
-
const SYNC_LABELED_RE = /^(
|
|
18
|
-
const ASYNC_LABELED_RE = /^(
|
|
16
|
+
// Forward (call) patterns — participant names may contain spaces, so use non-greedy (.+?)
|
|
17
|
+
const SYNC_LABELED_RE = /^(.+?)\s+-(.+)->\s+(.+)$/;
|
|
18
|
+
const ASYNC_LABELED_RE = /^(.+?)\s+~(.+)~>\s+(.+)$/;
|
|
19
19
|
|
|
20
20
|
// Deprecated patterns — produce errors
|
|
21
|
-
const RETURN_SYNC_LABELED_RE = /^(
|
|
22
|
-
const RETURN_ASYNC_LABELED_RE = /^(
|
|
23
|
-
const BIDI_SYNC_RE = /^(
|
|
24
|
-
const BIDI_ASYNC_RE = /^(
|
|
21
|
+
const RETURN_SYNC_LABELED_RE = /^(.+?)\s+<-(.+)-\s+(.+)$/;
|
|
22
|
+
const RETURN_ASYNC_LABELED_RE = /^(.+?)\s+<~(.+)~\s+(.+)$/;
|
|
23
|
+
const BIDI_SYNC_RE = /^(.+?)\s+<-(.+)->\s+(.+)$/;
|
|
24
|
+
const BIDI_ASYNC_RE = /^(.+?)\s+<~(.+)~>\s+(.+)$/;
|
|
25
25
|
|
|
26
26
|
const ARROW_CHARS = ['->', '~>'];
|
|
27
27
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Duration & Business Day Arithmetic
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import type { Duration, DurationUnit, GanttHolidays, Offset, Weekday } from '../gantt/types';
|
|
6
|
+
|
|
7
|
+
// ── Weekday constants ─────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/** JS Date.getDay() → Weekday mapping (0=Sun, 1=Mon, ..., 6=Sat) */
|
|
10
|
+
const JS_DAY_TO_WEEKDAY: Weekday[] = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if a date is a workday (not a weekend and not a holiday).
|
|
14
|
+
*/
|
|
15
|
+
export function isWorkday(
|
|
16
|
+
date: Date,
|
|
17
|
+
workweek: Weekday[],
|
|
18
|
+
holidaySet: Set<string>,
|
|
19
|
+
): boolean {
|
|
20
|
+
const dayName = JS_DAY_TO_WEEKDAY[date.getDay()];
|
|
21
|
+
if (!workweek.includes(dayName)) return false;
|
|
22
|
+
if (holidaySet.has(formatDateKey(date))) return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format a Date as YYYY-MM-DD for holiday set lookups.
|
|
28
|
+
*/
|
|
29
|
+
export function formatDateKey(date: Date): string {
|
|
30
|
+
const y = date.getFullYear();
|
|
31
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
32
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
33
|
+
return `${y}-${m}-${d}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a Set of holiday date strings for efficient lookup.
|
|
38
|
+
* Expands date ranges into individual dates.
|
|
39
|
+
*/
|
|
40
|
+
export function buildHolidaySet(holidays: GanttHolidays): Set<string> {
|
|
41
|
+
const set = new Set<string>();
|
|
42
|
+
|
|
43
|
+
for (const h of holidays.dates) {
|
|
44
|
+
set.add(h.date);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const range of holidays.ranges) {
|
|
48
|
+
const start = new Date(range.startDate + 'T00:00:00');
|
|
49
|
+
const end = new Date(range.endDate + 'T00:00:00');
|
|
50
|
+
const current = new Date(start);
|
|
51
|
+
while (current <= end) {
|
|
52
|
+
set.add(formatDateKey(current));
|
|
53
|
+
current.setDate(current.getDate() + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return set;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add business days to a start date, skipping weekends and holidays.
|
|
62
|
+
*
|
|
63
|
+
* For fractional business days, rounds to the nearest whole day first.
|
|
64
|
+
* Handles both positive amounts (forward) and zero (returns start date).
|
|
65
|
+
*/
|
|
66
|
+
export function addBusinessDays(
|
|
67
|
+
startDate: Date,
|
|
68
|
+
count: number,
|
|
69
|
+
workweek: Weekday[],
|
|
70
|
+
holidaySet: Set<string>,
|
|
71
|
+
direction: 1 | -1 = 1,
|
|
72
|
+
): Date {
|
|
73
|
+
const days = Math.round(Math.abs(count));
|
|
74
|
+
if (days === 0) return new Date(startDate);
|
|
75
|
+
|
|
76
|
+
const current = new Date(startDate);
|
|
77
|
+
let remaining = days;
|
|
78
|
+
|
|
79
|
+
while (remaining > 0) {
|
|
80
|
+
current.setDate(current.getDate() + direction);
|
|
81
|
+
if (isWorkday(current, workweek, holidaySet)) {
|
|
82
|
+
remaining--;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Add a gantt duration to a start date, producing an end date.
|
|
91
|
+
*
|
|
92
|
+
* Calendar units (d, w, m, q, y) ignore holidays.
|
|
93
|
+
* Business day units (bd) skip weekends and holidays.
|
|
94
|
+
*/
|
|
95
|
+
export function addGanttDuration(
|
|
96
|
+
startDate: Date,
|
|
97
|
+
duration: Duration,
|
|
98
|
+
holidays: GanttHolidays,
|
|
99
|
+
holidaySet: Set<string>,
|
|
100
|
+
direction: 1 | -1 = 1,
|
|
101
|
+
): Date {
|
|
102
|
+
const { amount, unit } = duration;
|
|
103
|
+
|
|
104
|
+
switch (unit) {
|
|
105
|
+
case 'bd':
|
|
106
|
+
return addBusinessDays(startDate, amount, holidays.workweek, holidaySet, direction);
|
|
107
|
+
|
|
108
|
+
case 'd': {
|
|
109
|
+
const result = new Date(startDate);
|
|
110
|
+
result.setDate(result.getDate() + Math.round(amount) * direction);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case 'w': {
|
|
115
|
+
const result = new Date(startDate);
|
|
116
|
+
result.setDate(result.getDate() + Math.round(amount * 7) * direction);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'm': {
|
|
121
|
+
const result = new Date(startDate);
|
|
122
|
+
const wholeMonths = direction === -1 ? Math.round(amount) : Math.floor(amount);
|
|
123
|
+
const fractionalDays = direction === -1 ? 0 : Math.round((amount - wholeMonths) * 30);
|
|
124
|
+
result.setMonth(result.getMonth() + wholeMonths * direction);
|
|
125
|
+
if (fractionalDays > 0) {
|
|
126
|
+
result.setDate(result.getDate() + fractionalDays * direction);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'q': {
|
|
132
|
+
const result = new Date(startDate);
|
|
133
|
+
const totalMonths = amount * 3;
|
|
134
|
+
const wholeMonths = direction === -1 ? Math.round(totalMonths) : Math.floor(totalMonths);
|
|
135
|
+
const fractionalDays = direction === -1 ? 0 : Math.round((totalMonths - wholeMonths) * 30);
|
|
136
|
+
result.setMonth(result.getMonth() + wholeMonths * direction);
|
|
137
|
+
if (fractionalDays > 0) {
|
|
138
|
+
result.setDate(result.getDate() + fractionalDays * direction);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case 'y': {
|
|
144
|
+
const result = new Date(startDate);
|
|
145
|
+
const wholeYears = direction === -1 ? Math.round(amount) : Math.floor(amount);
|
|
146
|
+
const fractionalMonths = direction === -1 ? 0 : Math.round((amount - wholeYears) * 12);
|
|
147
|
+
result.setFullYear(result.getFullYear() + wholeYears * direction);
|
|
148
|
+
if (fractionalMonths > 0) {
|
|
149
|
+
result.setMonth(result.getMonth() + fractionalMonths * direction);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse a duration string like "3bd" or "5d".
|
|
158
|
+
*/
|
|
159
|
+
export function parseDuration(s: string): Duration | null {
|
|
160
|
+
const match = s.trim().match(/^(\d+(?:\.\d+)?)(d|bd|w|m|q|y)$/);
|
|
161
|
+
if (!match) return null;
|
|
162
|
+
return { amount: parseFloat(match[1]), unit: match[2] as DurationUnit };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse an offset string like "5bd", "-3bd", or "0d".
|
|
167
|
+
* Returns null if the format is invalid.
|
|
168
|
+
* Explicit '+' prefix (e.g. "+5bd") returns null — caller should warn.
|
|
169
|
+
*/
|
|
170
|
+
export function parseOffset(value: string): Offset | null {
|
|
171
|
+
const trimmed = value.trim();
|
|
172
|
+
let direction: 1 | -1 = 1;
|
|
173
|
+
let remainder = trimmed;
|
|
174
|
+
|
|
175
|
+
if (trimmed.startsWith('-')) {
|
|
176
|
+
direction = -1;
|
|
177
|
+
remainder = trimmed.slice(1);
|
|
178
|
+
} else if (trimmed.startsWith('+')) {
|
|
179
|
+
return null; // explicit + is not supported
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const duration = parseDuration(remainder);
|
|
183
|
+
if (!duration) return null;
|
|
184
|
+
return { duration, direction };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Parse a date string (YYYY-MM-DD, YYYY-MM, or YYYY) into a Date object.
|
|
189
|
+
* Always returns midnight local time on the first available day.
|
|
190
|
+
*/
|
|
191
|
+
export function parseGanttDate(s: string): Date {
|
|
192
|
+
const parts = s.split('-').map(p => parseInt(p, 10));
|
|
193
|
+
const year = parts[0];
|
|
194
|
+
const month = parts.length >= 2 ? parts[1] - 1 : 0; // JS months are 0-based
|
|
195
|
+
const day = parts.length >= 3 ? parts[2] : 1;
|
|
196
|
+
return new Date(year, month, day);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Format a Date as YYYY-MM-DD string.
|
|
201
|
+
*/
|
|
202
|
+
export function formatGanttDate(date: Date): string {
|
|
203
|
+
return formatDateKey(date);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Calculate the difference in calendar days between two dates.
|
|
208
|
+
*/
|
|
209
|
+
export function daysBetween(a: Date, b: Date): number {
|
|
210
|
+
const msPerDay = 86400000;
|
|
211
|
+
return Math.round((b.getTime() - a.getTime()) / msPerDay);
|
|
212
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { FONT_FAMILY } from '../fonts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates an offscreen DOM container at the given dimensions, runs `fn` inside it,
|
|
5
|
+
* then removes it (try/finally). Returns whatever `fn` returns.
|
|
6
|
+
*/
|
|
7
|
+
export function runInExportContainer<T>(
|
|
8
|
+
width: number,
|
|
9
|
+
height: number,
|
|
10
|
+
fn: (container: HTMLDivElement) => T,
|
|
11
|
+
): T {
|
|
12
|
+
const container = document.createElement('div');
|
|
13
|
+
container.style.width = `${width}px`;
|
|
14
|
+
container.style.height = `${height}px`;
|
|
15
|
+
container.style.position = 'absolute';
|
|
16
|
+
container.style.left = '-9999px';
|
|
17
|
+
document.body.appendChild(container);
|
|
18
|
+
try {
|
|
19
|
+
return fn(container);
|
|
20
|
+
} finally {
|
|
21
|
+
document.body.removeChild(container);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extracts the SVG element from an export container, applies required export attributes
|
|
27
|
+
* (xmlns, fontFamily, transparent background if requested), and returns its outerHTML.
|
|
28
|
+
* Returns '' if no SVG element is found.
|
|
29
|
+
*/
|
|
30
|
+
export function extractExportSvg(
|
|
31
|
+
container: HTMLElement,
|
|
32
|
+
theme: 'light' | 'dark' | 'transparent',
|
|
33
|
+
): string {
|
|
34
|
+
const svgEl = container.querySelector('svg');
|
|
35
|
+
if (!svgEl) return '';
|
|
36
|
+
if (theme === 'transparent') svgEl.style.background = 'none';
|
|
37
|
+
svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
38
|
+
svgEl.style.fontFamily = FONT_FAMILY;
|
|
39
|
+
return svgEl.outerHTML;
|
|
40
|
+
}
|