@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.
Files changed (74) hide show
  1. package/dist/cli.cjs +92 -131
  2. package/dist/editor.cjs +13 -1
  3. package/dist/editor.cjs.map +1 -1
  4. package/dist/editor.js +13 -1
  5. package/dist/editor.js.map +1 -1
  6. package/dist/highlight.cjs +13 -1
  7. package/dist/highlight.cjs.map +1 -1
  8. package/dist/highlight.js +13 -1
  9. package/dist/highlight.js.map +1 -1
  10. package/dist/index.cjs +4524 -1511
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +427 -186
  13. package/dist/index.d.ts +427 -186
  14. package/dist/index.js +4526 -1503
  15. package/dist/index.js.map +1 -1
  16. package/docs/guide/chart-mindmap.md +198 -0
  17. package/docs/guide/chart-sequence.md +23 -1
  18. package/docs/guide/chart-wireframe.md +100 -0
  19. package/docs/guide/index.md +8 -0
  20. package/docs/language-reference.md +210 -2
  21. package/package.json +22 -9
  22. package/src/boxes-and-lines/collapse.ts +21 -3
  23. package/src/boxes-and-lines/layout.ts +51 -9
  24. package/src/boxes-and-lines/parser.ts +16 -4
  25. package/src/boxes-and-lines/renderer.ts +121 -23
  26. package/src/boxes-and-lines/types.ts +1 -0
  27. package/src/c4/parser.ts +8 -7
  28. package/src/class/parser.ts +6 -0
  29. package/src/cli.ts +1 -9
  30. package/src/completion.ts +26 -0
  31. package/src/d3.ts +169 -266
  32. package/src/dgmo-router.ts +103 -5
  33. package/src/diagnostics.ts +16 -6
  34. package/src/echarts.ts +43 -10
  35. package/src/editor/keywords.ts +12 -0
  36. package/src/er/parser.ts +22 -2
  37. package/src/gantt/renderer.ts +2 -2
  38. package/src/graph/flowchart-parser.ts +89 -52
  39. package/src/graph/layout.ts +73 -9
  40. package/src/graph/state-collapse.ts +78 -0
  41. package/src/graph/state-parser.ts +60 -35
  42. package/src/graph/state-renderer.ts +139 -34
  43. package/src/index.ts +41 -16
  44. package/src/infra/parser.ts +9 -2
  45. package/src/kanban/renderer.ts +305 -59
  46. package/src/mindmap/collapse.ts +88 -0
  47. package/src/mindmap/layout.ts +605 -0
  48. package/src/mindmap/parser.ts +379 -0
  49. package/src/mindmap/renderer.ts +543 -0
  50. package/src/mindmap/text-wrap.ts +207 -0
  51. package/src/mindmap/types.ts +55 -0
  52. package/src/palettes/color-utils.ts +4 -12
  53. package/src/palettes/index.ts +0 -4
  54. package/src/render.ts +31 -20
  55. package/src/sequence/parser.ts +7 -2
  56. package/src/sequence/renderer.ts +141 -21
  57. package/src/sharing.ts +2 -0
  58. package/src/sitemap/layout.ts +35 -12
  59. package/src/sitemap/renderer.ts +1 -6
  60. package/src/utils/arrows.ts +180 -11
  61. package/src/utils/d3-types.ts +4 -0
  62. package/src/utils/export-container.ts +3 -2
  63. package/src/utils/legend-constants.ts +0 -4
  64. package/src/utils/legend-d3.ts +1 -0
  65. package/src/utils/legend-layout.ts +2 -2
  66. package/src/utils/parsing.ts +2 -0
  67. package/src/utils/time-ticks.ts +213 -0
  68. package/src/wireframe/layout.ts +460 -0
  69. package/src/wireframe/parser.ts +956 -0
  70. package/src/wireframe/renderer.ts +1293 -0
  71. package/src/wireframe/types.ts +110 -0
  72. package/src/branding.ts +0 -67
  73. package/src/dgmo-mermaid.ts +0 -262
  74. package/src/palettes/mermaid-bridge.ts +0 -220
@@ -0,0 +1,207 @@
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
+ const CHAR_WIDTH_RATIO = 0.58; // avg char width / fontSize for Helvetica
10
+ const H_PAD = 16; // 8px padding each side
11
+ const MAX_LABEL_LINES = 3;
12
+ const MAX_DESC_LINES = 2;
13
+
14
+ /** Split text into tokens, keeping hyphens attached to the left word. */
15
+ function tokenize(text: string): string[] {
16
+ const tokens: string[] = [];
17
+ // Split on spaces, and after hyphens (keep hyphen with left token)
18
+ const parts = text.split(/(\s+)/);
19
+ for (const part of parts) {
20
+ if (/^\s+$/.test(part)) continue; // skip whitespace tokens
21
+ // Further split on hyphens: "well-known" → ["well-", "known"]
22
+ const hyphenParts = part.split(/(?<=-)(?=\S)/);
23
+ tokens.push(...hyphenParts);
24
+ }
25
+ return tokens;
26
+ }
27
+
28
+ /**
29
+ * Wrap text into lines that fit within maxWidth at the given fontSize.
30
+ * Returns null if the text doesn't fit within maxLines.
31
+ */
32
+ function tryWrap(
33
+ tokens: string[],
34
+ maxWidth: number,
35
+ fontSize: number,
36
+ maxLines: number
37
+ ): string[] | null {
38
+ const availWidth = maxWidth - H_PAD;
39
+ const charWidth = fontSize * CHAR_WIDTH_RATIO;
40
+ const maxChars = Math.max(1, Math.floor(availWidth / charWidth));
41
+
42
+ const lines: string[] = [];
43
+ let currentLine = '';
44
+
45
+ for (const token of tokens) {
46
+ // After a hyphen-ending token, don't add a space
47
+ const sep = currentLine && !currentLine.endsWith('-') ? ' ' : '';
48
+ const candidate = currentLine + sep + token;
49
+
50
+ if (candidate.length <= maxChars) {
51
+ currentLine = candidate;
52
+ } else if (!currentLine) {
53
+ // Single token exceeds line — force it onto this line (will be truncated later if needed)
54
+ currentLine = token;
55
+ } else {
56
+ // Push current line, start new one
57
+ lines.push(currentLine);
58
+ if (lines.length >= maxLines) {
59
+ // Can't fit — return null to signal overflow
60
+ return null;
61
+ }
62
+ currentLine = token;
63
+ }
64
+ }
65
+ if (currentLine) {
66
+ lines.push(currentLine);
67
+ }
68
+
69
+ if (lines.length > maxLines) return null;
70
+ return lines;
71
+ }
72
+
73
+ /** Truncate the last line of a lines array with ellipsis to fit maxChars. */
74
+ function truncateLastLine(
75
+ lines: string[],
76
+ maxWidth: number,
77
+ fontSize: number
78
+ ): string[] {
79
+ const availWidth = maxWidth - H_PAD;
80
+ const charWidth = fontSize * CHAR_WIDTH_RATIO;
81
+ const maxChars = Math.max(1, Math.floor(availWidth / charWidth));
82
+
83
+ const result = [...lines];
84
+ const last = result[result.length - 1];
85
+ if (last.length > maxChars) {
86
+ result[result.length - 1] = last.substring(0, maxChars - 1) + '\u2026';
87
+ }
88
+ return result;
89
+ }
90
+
91
+ interface WrapResult {
92
+ lines: string[];
93
+ fontSize: number;
94
+ }
95
+
96
+ /**
97
+ * Wrap text to fit within a node of the given maxWidth.
98
+ * Tries wrapping at baseFontSize first. If text doesn't fit within
99
+ * maxLines, reduces font size by 1px at a time down to minFontSize.
100
+ * As a last resort, truncates the final line with ellipsis.
101
+ */
102
+ export function wrapText(
103
+ text: string,
104
+ maxWidth: number,
105
+ baseFontSize: number,
106
+ minFontSize: number,
107
+ maxLines: number = MAX_LABEL_LINES
108
+ ): WrapResult {
109
+ if (!text) return { lines: [''], fontSize: baseFontSize };
110
+
111
+ const tokens = tokenize(text);
112
+
113
+ // Try at each font size from base down to min
114
+ for (let fs = baseFontSize; fs >= minFontSize; fs--) {
115
+ const lines = tryWrap(tokens, maxWidth, fs, maxLines);
116
+ if (lines) {
117
+ // Ensure each line fits (truncate overly long single tokens)
118
+ return { lines: truncateLastLine(lines, maxWidth, fs), fontSize: fs };
119
+ }
120
+ }
121
+
122
+ // Last resort: wrap at minFontSize with unlimited lines, then take first maxLines
123
+ const allLines = tryWrap(tokens, maxWidth, minFontSize, 999) ?? [text];
124
+ const capped = allLines.slice(0, maxLines);
125
+ const truncated = truncateLastLine(capped, maxWidth, minFontSize);
126
+ // If we dropped lines, append ellipsis to indicate overflow
127
+ if (allLines.length > maxLines) {
128
+ const last = truncated[truncated.length - 1];
129
+ if (!last.endsWith('\u2026')) {
130
+ const availWidth = maxWidth - H_PAD;
131
+ const charWidth = minFontSize * CHAR_WIDTH_RATIO;
132
+ const maxChars = Math.max(1, Math.floor(availWidth / charWidth));
133
+ if (last.length >= maxChars - 1) {
134
+ truncated[truncated.length - 1] =
135
+ last.substring(0, maxChars - 1) + '\u2026';
136
+ } else {
137
+ truncated[truncated.length - 1] = last + '\u2026';
138
+ }
139
+ }
140
+ }
141
+ return {
142
+ lines: truncated,
143
+ fontSize: minFontSize,
144
+ };
145
+ }
146
+
147
+ // ============================================================
148
+ // Compute full node text layout (shared between layout + renderer)
149
+ // ============================================================
150
+
151
+ const ROOT_FONT_SIZE = 17;
152
+ const MIN_FONT_SIZE = 9;
153
+ const FONT_STEP = 3;
154
+ const DESC_FONT_SIZE = 10;
155
+
156
+ function labelFontSize(depth: number): number {
157
+ return Math.max(MIN_FONT_SIZE, ROOT_FONT_SIZE - depth * FONT_STEP);
158
+ }
159
+
160
+ interface NodeTextLayout {
161
+ labelLines: string[];
162
+ labelFontSize: number;
163
+ descLines: string[];
164
+ descFontSize: number;
165
+ }
166
+
167
+ /**
168
+ * Compute wrapped text layout for a mindmap node.
169
+ * Called by both layout (for height) and renderer (for drawing).
170
+ */
171
+ export function computeNodeText(
172
+ label: string,
173
+ description: string | undefined,
174
+ depth: number,
175
+ nodeWidth: number,
176
+ hideDescriptions: boolean
177
+ ): NodeTextLayout {
178
+ const baseFontSize = labelFontSize(depth);
179
+ const labelResult = wrapText(
180
+ label,
181
+ nodeWidth,
182
+ baseFontSize,
183
+ MIN_FONT_SIZE,
184
+ MAX_LABEL_LINES
185
+ );
186
+
187
+ let descLines: string[] = [];
188
+ let descFontSize = DESC_FONT_SIZE;
189
+ if (!hideDescriptions && description) {
190
+ const descResult = wrapText(
191
+ description,
192
+ nodeWidth,
193
+ DESC_FONT_SIZE,
194
+ DESC_FONT_SIZE, // don't shrink descriptions
195
+ MAX_DESC_LINES
196
+ );
197
+ descLines = descResult.lines;
198
+ descFontSize = descResult.fontSize;
199
+ }
200
+
201
+ return {
202
+ labelLines: labelResult.lines,
203
+ labelFontSize: labelResult.fontSize,
204
+ descLines,
205
+ descFontSize,
206
+ };
207
+ }
@@ -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
+ }
@@ -84,17 +84,6 @@ export function hexToHSLString(hex: string): string {
84
84
  // Color Manipulation
85
85
  // ============================================================
86
86
 
87
- /**
88
- * Derive a muted (desaturated, darkened) variant of a color.
89
- * Used by the Mermaid theme generator for dark-mode fills.
90
- *
91
- * Algorithm: cap saturation at 35% and lightness at 36%.
92
- */
93
- export function mute(hex: string): string {
94
- const { h, s, l } = hexToHSL(hex);
95
- return hslToHex(h, Math.min(s, 35), Math.min(l, 36));
96
- }
97
-
98
87
  /**
99
88
  * Blend a color toward white (light mode quadrant fills).
100
89
  * amount: 0 = original, 1 = white
@@ -232,7 +221,10 @@ export function getSeriesColors(palette: PaletteColors): string[] {
232
221
  * saturation and lightness, guaranteeing every segment gets a unique,
233
222
  * perceptually distinct color regardless of segment count.
234
223
  */
235
- export function getSegmentColors(palette: PaletteColors, count: number): string[] {
224
+ export function getSegmentColors(
225
+ palette: PaletteColors,
226
+ count: number
227
+ ): string[] {
236
228
  const base = getSeriesColors(palette);
237
229
  const unique = [...new Set(base)];
238
230
  const hsls = unique.map(hexToHSL);
@@ -14,7 +14,6 @@ export {
14
14
  hexToHSL,
15
15
  hslToHex,
16
16
  hexToHSLString,
17
- mute,
18
17
  tint,
19
18
  shade,
20
19
  getSeriesColors,
@@ -34,6 +33,3 @@ export { tokyoNightPalette } from './tokyo-night';
34
33
 
35
34
  export { draculaPalette } from './dracula';
36
35
  export { monokaiPalette } from './monokai';
37
-
38
- // Re-export Mermaid bridge
39
- export { buildMermaidThemeVars, buildThemeCSS } from './mermaid-bridge';
package/src/render.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { renderForExport } from './d3';
2
2
  import { renderExtendedChartForExport } from './echarts';
3
- import { parseDgmoChartType, getRenderCategory } from './dgmo-router';
3
+ import {
4
+ parseDgmoChartType,
5
+ getRenderCategory,
6
+ parseDgmo,
7
+ } from './dgmo-router';
8
+ import type { DgmoError } from './diagnostics';
4
9
  import { getPalette } from './palettes/registry';
10
+ import type { CompactViewState } from './sharing';
5
11
 
6
12
  /**
7
13
  * Ensures DOM globals are available for D3 renderers.
@@ -45,13 +51,13 @@ async function ensureDom(): Promise<void> {
45
51
  *
46
52
  * @param content - DGMO source text
47
53
  * @param options - Optional theme and palette settings
48
- * @returns SVG string, or empty string on error
54
+ * @returns Object with `svg` (SVG string, empty on error) and `diagnostics` (parse errors/warnings)
49
55
  *
50
56
  * @example
51
57
  * ```ts
52
58
  * import { render } from '@diagrammo/dgmo';
53
59
  *
54
- * const svg = await render(`pie Languages
60
+ * const { svg, diagnostics } = await render(`pie Languages
55
61
  * TypeScript: 45
56
62
  * Python: 30
57
63
  * Rust: 25`);
@@ -62,48 +68,53 @@ export async function render(
62
68
  options?: {
63
69
  theme?: 'light' | 'dark' | 'transparent';
64
70
  palette?: string;
65
- branding?: boolean;
66
71
  c4Level?: 'context' | 'containers' | 'components' | 'deployment';
67
72
  c4System?: string;
68
73
  c4Container?: string;
69
74
  tagGroup?: string;
70
75
  /** Legend state for export — controls which tag group is shown in exported SVG. */
71
76
  legendState?: { activeGroup?: string; hiddenAttributes?: string[] };
77
+ /** View state for export — controls interactive state (collapse, swimlanes, etc.) */
78
+ viewState?: CompactViewState;
72
79
  }
73
- ): Promise<string> {
80
+ ): Promise<{ svg: string; diagnostics: DgmoError[] }> {
74
81
  const theme = options?.theme ?? 'light';
75
82
  const paletteName = options?.palette ?? 'nord';
76
- const branding = options?.branding ?? false;
77
83
 
78
84
  const paletteColors =
79
85
  getPalette(paletteName)[theme === 'dark' ? 'dark' : 'light'];
80
86
 
87
+ const { diagnostics } = parseDgmo(content);
88
+
81
89
  const chartType = parseDgmoChartType(content);
82
90
  const category = chartType ? getRenderCategory(chartType) : null;
83
91
 
84
- // Build orgExportState from legendState if provided
85
- const legendExportState = options?.legendState
86
- ? {
87
- activeTagGroup: options.legendState.activeGroup ?? null,
88
- hiddenAttributes: options.legendState.hiddenAttributes
89
- ? new Set(options.legendState.hiddenAttributes)
90
- : undefined,
91
- }
92
- : 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);
93
101
 
94
102
  if (category === 'data-chart') {
95
- return renderExtendedChartForExport(content, theme, paletteColors, {
96
- branding,
97
- });
103
+ const svg = await renderExtendedChartForExport(
104
+ content,
105
+ theme,
106
+ paletteColors
107
+ );
108
+ return { svg, diagnostics };
98
109
  }
99
110
 
100
111
  // Visualization/diagram and unknown/null types all go through the unified renderer
101
112
  await ensureDom();
102
- return renderForExport(content, theme, paletteColors, legendExportState, {
103
- branding,
113
+ const svg = await renderForExport(content, theme, paletteColors, viewState, {
104
114
  c4Level: options?.c4Level,
105
115
  c4System: options?.c4System,
106
116
  c4Container: options?.c4Container,
107
117
  tagGroup: options?.tagGroup,
108
118
  });
119
+ return { svg, diagnostics };
109
120
  }
@@ -5,7 +5,7 @@
5
5
  import { inferParticipantType } from './participant-inference';
6
6
  import type { DgmoError } from '../diagnostics';
7
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
8
- import { parseArrow } from '../utils/arrows';
8
+ import { parseArrow, parseInArrowLabel } from '../utils/arrows';
9
9
  import {
10
10
  measureIndent,
11
11
  extractColor,
@@ -945,9 +945,14 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
945
945
  }
946
946
  if (labeledArrow) {
947
947
  contentStarted = true;
948
- const { from, to, label, async: isAsync } = labeledArrow;
948
+ const { from, to, label: rawLabel, async: isAsync } = labeledArrow;
949
949
  lastMsgFrom = from;
950
950
 
951
+ // TD-13/TD-14: validate in-arrow label characters
952
+ const labelResult = parseInArrowLabel(rawLabel, lineNumber);
953
+ labelResult.diagnostics.forEach((d) => result.diagnostics.push(d));
954
+ const label = labelResult.label ?? rawLabel;
955
+
951
956
  const msg: SequenceMessage = {
952
957
  from,
953
958
  to,