@diagrammo/dgmo 0.8.20 → 0.8.22

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.
Files changed (110) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -0
  3. package/dist/cli.cjs +142 -90
  4. package/dist/editor.cjs +30 -4
  5. package/dist/editor.cjs.map +1 -1
  6. package/dist/editor.js +30 -4
  7. package/dist/editor.js.map +1 -1
  8. package/dist/highlight.cjs +25 -3
  9. package/dist/highlight.cjs.map +1 -1
  10. package/dist/highlight.js +25 -3
  11. package/dist/highlight.js.map +1 -1
  12. package/dist/index.cjs +21201 -12886
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +646 -89
  15. package/dist/index.d.ts +646 -89
  16. package/dist/index.js +21178 -12889
  17. package/dist/index.js.map +1 -1
  18. package/docs/guide/chart-mindmap.md +198 -0
  19. package/docs/guide/chart-sequence.md +23 -1
  20. package/docs/guide/chart-sitemap.md +18 -1
  21. package/docs/guide/chart-tech-radar.md +219 -0
  22. package/docs/guide/chart-wireframe.md +100 -0
  23. package/docs/guide/index.md +8 -0
  24. package/docs/guide/registry.json +1 -0
  25. package/docs/language-reference.md +249 -4
  26. package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
  27. package/gallery/fixtures/c4-full.dgmo +2 -2
  28. package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
  29. package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
  30. package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
  31. package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
  32. package/gallery/fixtures/gantt-full.dgmo +2 -2
  33. package/gallery/fixtures/gantt.dgmo +2 -2
  34. package/gallery/fixtures/infra-full.dgmo +2 -2
  35. package/gallery/fixtures/infra.dgmo +1 -1
  36. package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
  37. package/gallery/fixtures/sequence-tags.dgmo +2 -2
  38. package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
  39. package/gallery/fixtures/tech-radar.dgmo +36 -0
  40. package/gallery/fixtures/timeline.dgmo +1 -1
  41. package/package.json +1 -1
  42. package/src/boxes-and-lines/collapse.ts +21 -3
  43. package/src/boxes-and-lines/layout.ts +360 -42
  44. package/src/boxes-and-lines/parser.ts +94 -11
  45. package/src/boxes-and-lines/renderer.ts +371 -114
  46. package/src/boxes-and-lines/types.ts +2 -1
  47. package/src/c4/layout.ts +8 -8
  48. package/src/c4/parser.ts +35 -2
  49. package/src/c4/renderer.ts +19 -3
  50. package/src/c4/types.ts +1 -0
  51. package/src/chart.ts +14 -7
  52. package/src/completion.ts +253 -0
  53. package/src/cycle/layout.ts +732 -0
  54. package/src/cycle/parser.ts +352 -0
  55. package/src/cycle/renderer.ts +539 -0
  56. package/src/cycle/types.ts +77 -0
  57. package/src/d3.ts +240 -40
  58. package/src/dgmo-router.ts +15 -0
  59. package/src/echarts.ts +7 -4
  60. package/src/editor/dgmo.grammar +5 -1
  61. package/src/editor/dgmo.grammar.js +1 -1
  62. package/src/editor/keywords.ts +26 -0
  63. package/src/gantt/parser.ts +2 -8
  64. package/src/graph/flowchart-parser.ts +15 -21
  65. package/src/graph/layout.ts +73 -9
  66. package/src/graph/state-collapse.ts +78 -0
  67. package/src/graph/state-parser.ts +5 -10
  68. package/src/graph/state-renderer.ts +139 -34
  69. package/src/index.ts +78 -0
  70. package/src/infra/layout.ts +218 -74
  71. package/src/infra/parser.ts +30 -6
  72. package/src/infra/renderer.ts +14 -8
  73. package/src/infra/types.ts +10 -3
  74. package/src/journey-map/layout.ts +386 -0
  75. package/src/journey-map/parser.ts +540 -0
  76. package/src/journey-map/renderer.ts +1456 -0
  77. package/src/journey-map/types.ts +47 -0
  78. package/src/kanban/parser.ts +3 -10
  79. package/src/kanban/renderer.ts +325 -63
  80. package/src/mindmap/collapse.ts +88 -0
  81. package/src/mindmap/layout.ts +605 -0
  82. package/src/mindmap/parser.ts +373 -0
  83. package/src/mindmap/renderer.ts +544 -0
  84. package/src/mindmap/text-wrap.ts +217 -0
  85. package/src/mindmap/types.ts +55 -0
  86. package/src/org/parser.ts +2 -6
  87. package/src/render.ts +18 -21
  88. package/src/sequence/renderer.ts +273 -56
  89. package/src/sharing.ts +3 -0
  90. package/src/sitemap/layout.ts +56 -18
  91. package/src/sitemap/parser.ts +26 -17
  92. package/src/sitemap/renderer.ts +34 -0
  93. package/src/sitemap/types.ts +1 -0
  94. package/src/tech-radar/index.ts +14 -0
  95. package/src/tech-radar/interactive.ts +1058 -0
  96. package/src/tech-radar/layout.ts +190 -0
  97. package/src/tech-radar/parser.ts +385 -0
  98. package/src/tech-radar/renderer.ts +1159 -0
  99. package/src/tech-radar/shared.ts +187 -0
  100. package/src/tech-radar/types.ts +81 -0
  101. package/src/utils/description-helpers.ts +33 -0
  102. package/src/utils/export-container.ts +3 -2
  103. package/src/utils/legend-d3.ts +1 -0
  104. package/src/utils/legend-layout.ts +5 -3
  105. package/src/utils/parsing.ts +48 -7
  106. package/src/utils/tag-groups.ts +46 -60
  107. package/src/wireframe/layout.ts +460 -0
  108. package/src/wireframe/parser.ts +956 -0
  109. package/src/wireframe/renderer.ts +1293 -0
  110. package/src/wireframe/types.ts +110 -0
@@ -0,0 +1,217 @@
1
+ // ============================================================
2
+ // Mindmap Text Wrapping
3
+ // ============================================================
4
+ //
5
+ // Shared logic for wrapping node labels and descriptions into
6
+ // multiple lines. Used by both layout (for sizing) and renderer
7
+ // (for drawing). Ensures both agree on line breaks and font size.
8
+
9
+ import { preprocessDescriptionLine } from '../utils/description-helpers';
10
+
11
+ const CHAR_WIDTH_RATIO = 0.58; // avg char width / fontSize for Helvetica
12
+ const H_PAD = 16; // 8px padding each side
13
+ const MAX_LABEL_LINES = 3;
14
+ const MAX_DESC_LINES = 2;
15
+
16
+ /** Split text into tokens, keeping hyphens attached to the left word. */
17
+ function tokenize(text: string): string[] {
18
+ const tokens: string[] = [];
19
+ // Split on spaces, and after hyphens (keep hyphen with left token)
20
+ const parts = text.split(/(\s+)/);
21
+ for (const part of parts) {
22
+ if (/^\s+$/.test(part)) continue; // skip whitespace tokens
23
+ // Further split on hyphens: "well-known" → ["well-", "known"]
24
+ const hyphenParts = part.split(/(?<=-)(?=\S)/);
25
+ tokens.push(...hyphenParts);
26
+ }
27
+ return tokens;
28
+ }
29
+
30
+ /**
31
+ * Wrap text into lines that fit within maxWidth at the given fontSize.
32
+ * Returns null if the text doesn't fit within maxLines.
33
+ */
34
+ function tryWrap(
35
+ tokens: string[],
36
+ maxWidth: number,
37
+ fontSize: number,
38
+ maxLines: number
39
+ ): string[] | null {
40
+ const availWidth = maxWidth - H_PAD;
41
+ const charWidth = fontSize * CHAR_WIDTH_RATIO;
42
+ const maxChars = Math.max(1, Math.floor(availWidth / charWidth));
43
+
44
+ const lines: string[] = [];
45
+ let currentLine = '';
46
+
47
+ for (const token of tokens) {
48
+ // After a hyphen-ending token, don't add a space
49
+ const sep = currentLine && !currentLine.endsWith('-') ? ' ' : '';
50
+ const candidate = currentLine + sep + token;
51
+
52
+ if (candidate.length <= maxChars) {
53
+ currentLine = candidate;
54
+ } else if (!currentLine) {
55
+ // Single token exceeds line — force it onto this line (will be truncated later if needed)
56
+ currentLine = token;
57
+ } else {
58
+ // Push current line, start new one
59
+ lines.push(currentLine);
60
+ if (lines.length >= maxLines) {
61
+ // Can't fit — return null to signal overflow
62
+ return null;
63
+ }
64
+ currentLine = token;
65
+ }
66
+ }
67
+ if (currentLine) {
68
+ lines.push(currentLine);
69
+ }
70
+
71
+ if (lines.length > maxLines) return null;
72
+ return lines;
73
+ }
74
+
75
+ /** Truncate the last line of a lines array with ellipsis to fit maxChars. */
76
+ function truncateLastLine(
77
+ lines: string[],
78
+ maxWidth: number,
79
+ fontSize: number
80
+ ): string[] {
81
+ const availWidth = maxWidth - H_PAD;
82
+ const charWidth = fontSize * CHAR_WIDTH_RATIO;
83
+ const maxChars = Math.max(1, Math.floor(availWidth / charWidth));
84
+
85
+ const result = [...lines];
86
+ const last = result[result.length - 1];
87
+ if (last.length > maxChars) {
88
+ result[result.length - 1] = last.substring(0, maxChars - 1) + '\u2026';
89
+ }
90
+ return result;
91
+ }
92
+
93
+ interface WrapResult {
94
+ lines: string[];
95
+ fontSize: number;
96
+ }
97
+
98
+ /**
99
+ * Wrap text to fit within a node of the given maxWidth.
100
+ * Tries wrapping at baseFontSize first. If text doesn't fit within
101
+ * maxLines, reduces font size by 1px at a time down to minFontSize.
102
+ * As a last resort, truncates the final line with ellipsis.
103
+ */
104
+ export function wrapText(
105
+ text: string,
106
+ maxWidth: number,
107
+ baseFontSize: number,
108
+ minFontSize: number,
109
+ maxLines: number = MAX_LABEL_LINES
110
+ ): WrapResult {
111
+ if (!text) return { lines: [''], fontSize: baseFontSize };
112
+
113
+ const tokens = tokenize(text);
114
+
115
+ // Try at each font size from base down to min
116
+ for (let fs = baseFontSize; fs >= minFontSize; fs--) {
117
+ const lines = tryWrap(tokens, maxWidth, fs, maxLines);
118
+ if (lines) {
119
+ // Ensure each line fits (truncate overly long single tokens)
120
+ return { lines: truncateLastLine(lines, maxWidth, fs), fontSize: fs };
121
+ }
122
+ }
123
+
124
+ // Last resort: wrap at minFontSize with unlimited lines, then take first maxLines
125
+ const allLines = tryWrap(tokens, maxWidth, minFontSize, 999) ?? [text];
126
+ const capped = allLines.slice(0, maxLines);
127
+ const truncated = truncateLastLine(capped, maxWidth, minFontSize);
128
+ // If we dropped lines, append ellipsis to indicate overflow
129
+ if (allLines.length > maxLines) {
130
+ const last = truncated[truncated.length - 1];
131
+ if (!last.endsWith('\u2026')) {
132
+ const availWidth = maxWidth - H_PAD;
133
+ const charWidth = minFontSize * CHAR_WIDTH_RATIO;
134
+ const maxChars = Math.max(1, Math.floor(availWidth / charWidth));
135
+ if (last.length >= maxChars - 1) {
136
+ truncated[truncated.length - 1] =
137
+ last.substring(0, maxChars - 1) + '\u2026';
138
+ } else {
139
+ truncated[truncated.length - 1] = last + '\u2026';
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ lines: truncated,
145
+ fontSize: minFontSize,
146
+ };
147
+ }
148
+
149
+ // ============================================================
150
+ // Compute full node text layout (shared between layout + renderer)
151
+ // ============================================================
152
+
153
+ const ROOT_FONT_SIZE = 17;
154
+ const MIN_FONT_SIZE = 9;
155
+ const FONT_STEP = 3;
156
+ const DESC_FONT_SIZE = 10;
157
+
158
+ function labelFontSize(depth: number): number {
159
+ return Math.max(MIN_FONT_SIZE, ROOT_FONT_SIZE - depth * FONT_STEP);
160
+ }
161
+
162
+ interface NodeTextLayout {
163
+ labelLines: string[];
164
+ labelFontSize: number;
165
+ descLines: string[];
166
+ descFontSize: number;
167
+ }
168
+
169
+ /**
170
+ * Compute wrapped text layout for a mindmap node.
171
+ * Called by both layout (for height) and renderer (for drawing).
172
+ */
173
+ export function computeNodeText(
174
+ label: string,
175
+ description: string[] | undefined,
176
+ depth: number,
177
+ nodeWidth: number,
178
+ hideDescriptions: boolean
179
+ ): NodeTextLayout {
180
+ const baseFontSize = labelFontSize(depth);
181
+ const labelResult = wrapText(
182
+ label,
183
+ nodeWidth,
184
+ baseFontSize,
185
+ MIN_FONT_SIZE,
186
+ MAX_LABEL_LINES
187
+ );
188
+
189
+ const descLines: string[] = [];
190
+ let descFontSize = DESC_FONT_SIZE;
191
+ if (!hideDescriptions && description && description.length > 0) {
192
+ // Wrap each description line independently so bullets and line breaks are preserved.
193
+ // Cap total output lines at MAX_DESC_LINES across all input lines.
194
+ let remaining = MAX_DESC_LINES;
195
+ for (const rawLine of description) {
196
+ if (remaining <= 0) break;
197
+ const line = preprocessDescriptionLine(rawLine);
198
+ const lineResult = wrapText(
199
+ line,
200
+ nodeWidth,
201
+ DESC_FONT_SIZE,
202
+ DESC_FONT_SIZE,
203
+ remaining
204
+ );
205
+ descLines.push(...lineResult.lines);
206
+ remaining -= lineResult.lines.length;
207
+ descFontSize = lineResult.fontSize;
208
+ }
209
+ }
210
+
211
+ return {
212
+ labelLines: labelResult.lines,
213
+ labelFontSize: labelResult.fontSize,
214
+ descLines,
215
+ descFontSize,
216
+ };
217
+ }
@@ -0,0 +1,55 @@
1
+ import type { DgmoError } from '../diagnostics.js';
2
+ import type { TagGroup } from '../utils/tag-groups.js';
3
+
4
+ export interface MindmapNode {
5
+ id: string;
6
+ label: string;
7
+ description?: string[];
8
+ metadata: Record<string, string>;
9
+ children: MindmapNode[];
10
+ parentId: string | null;
11
+ lineNumber: number;
12
+ color?: string;
13
+ collapsed?: boolean;
14
+ }
15
+
16
+ export interface ParsedMindmap {
17
+ title: string | null;
18
+ titleLineNumber: number | null;
19
+ roots: MindmapNode[];
20
+ tagGroups: TagGroup[];
21
+ options: Record<string, string>;
22
+ diagnostics: DgmoError[];
23
+ error: string | null;
24
+ }
25
+
26
+ export interface MindmapLayoutNode {
27
+ id: string;
28
+ label: string;
29
+ description?: string[];
30
+ metadata: Record<string, string>;
31
+ lineNumber: number;
32
+ color?: string;
33
+ x: number;
34
+ y: number;
35
+ width: number;
36
+ height: number;
37
+ depth: number;
38
+ angle: number;
39
+ radius: number;
40
+ hiddenCount?: number;
41
+ hasChildren?: boolean;
42
+ }
43
+
44
+ export interface MindmapLayoutEdge {
45
+ sourceId: string;
46
+ targetId: string;
47
+ path: string; // SVG path d attribute
48
+ }
49
+
50
+ export interface MindmapLayoutResult {
51
+ nodes: MindmapLayoutNode[];
52
+ edges: MindmapLayoutEdge[];
53
+ width: number;
54
+ height: number;
55
+ }
package/src/org/parser.ts CHANGED
@@ -270,8 +270,7 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
270
270
 
271
271
  if (containerMatch) {
272
272
  // It's a container node
273
- const rawLabel = containerMatch[1].trim();
274
- const { label, color } = extractColor(rawLabel, palette);
273
+ const label = containerMatch[1].trim();
275
274
 
276
275
  containerCounter++;
277
276
  const node: OrgNode = {
@@ -282,7 +281,6 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
282
281
  parentId: null,
283
282
  isContainer: true,
284
283
  lineNumber,
285
- color,
286
284
  };
287
285
 
288
286
  attachNode(node, indent, indentStack, result);
@@ -378,8 +376,7 @@ function parseNodeLabel(
378
376
  // Check for single-line compact metadata: "Alice Park | role: Senior, location: NY"
379
377
  const segments = trimmed.split('|').map((s) => s.trim());
380
378
 
381
- const rawLabel = segments[0];
382
- const { label, color } = extractColor(rawLabel, palette);
379
+ const label = segments[0];
383
380
 
384
381
  const metadata = parsePipeMetadata(
385
382
  segments,
@@ -395,7 +392,6 @@ function parseNodeLabel(
395
392
  parentId: null,
396
393
  isContainer: false,
397
394
  lineNumber,
398
- color,
399
395
  };
400
396
  }
401
397
 
package/src/render.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from './dgmo-router';
8
8
  import type { DgmoError } from './diagnostics';
9
9
  import { getPalette } from './palettes/registry';
10
+ import type { CompactViewState } from './sharing';
10
11
 
11
12
  /**
12
13
  * Ensures DOM globals are available for D3 renderers.
@@ -73,6 +74,8 @@ export async function render(
73
74
  tagGroup?: string;
74
75
  /** Legend state for export — controls which tag group is shown in exported SVG. */
75
76
  legendState?: { activeGroup?: string; hiddenAttributes?: string[] };
77
+ /** View state for export — controls interactive state (collapse, swimlanes, etc.) */
78
+ viewState?: CompactViewState;
76
79
  }
77
80
  ): Promise<{ svg: string; diagnostics: DgmoError[] }> {
78
81
  const theme = options?.theme ?? 'light';
@@ -86,15 +89,15 @@ export async function render(
86
89
  const chartType = parseDgmoChartType(content);
87
90
  const category = chartType ? getRenderCategory(chartType) : null;
88
91
 
89
- // Build orgExportState from legendState if provided
90
- const legendExportState = options?.legendState
91
- ? {
92
- activeTagGroup: options.legendState.activeGroup ?? null,
93
- hiddenAttributes: options.legendState.hiddenAttributes
94
- ? new Set(options.legendState.hiddenAttributes)
95
- : undefined,
96
- }
97
- : undefined;
92
+ // Build viewState from legendState (backwards compat) or use provided viewState
93
+ const viewState: CompactViewState | undefined =
94
+ options?.viewState ??
95
+ (options?.legendState
96
+ ? {
97
+ tag: options.legendState.activeGroup ?? undefined,
98
+ ha: options.legendState.hiddenAttributes,
99
+ }
100
+ : undefined);
98
101
 
99
102
  if (category === 'data-chart') {
100
103
  const svg = await renderExtendedChartForExport(
@@ -107,17 +110,11 @@ export async function render(
107
110
 
108
111
  // Visualization/diagram and unknown/null types all go through the unified renderer
109
112
  await ensureDom();
110
- const svg = await renderForExport(
111
- content,
112
- theme,
113
- paletteColors,
114
- legendExportState,
115
- {
116
- c4Level: options?.c4Level,
117
- c4System: options?.c4System,
118
- c4Container: options?.c4Container,
119
- tagGroup: options?.tagGroup,
120
- }
121
- );
113
+ const svg = await renderForExport(content, theme, paletteColors, viewState, {
114
+ c4Level: options?.c4Level,
115
+ c4System: options?.c4System,
116
+ c4Container: options?.c4Container,
117
+ tagGroup: options?.tagGroup,
118
+ });
122
119
  return { svg, diagnostics };
123
120
  }