@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,373 @@
1
+ import type { PaletteColors } from '../palettes';
2
+ import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
3
+ import type { TagGroup } from '../utils/tag-groups';
4
+ import {
5
+ matchTagBlockHeading,
6
+ validateTagValues,
7
+ validateTagGroupNames,
8
+ stripDefaultModifier,
9
+ } from '../utils/tag-groups';
10
+ import {
11
+ measureIndent,
12
+ extractColor,
13
+ parsePipeMetadata,
14
+ MULTIPLE_PIPE_ERROR,
15
+ parseFirstLine,
16
+ OPTION_NOCOLON_RE,
17
+ } from '../utils/parsing';
18
+ import type { MindmapNode, ParsedMindmap } from './types';
19
+ import { tryStripDescriptionKeyword } from '../utils/description-helpers';
20
+
21
+ // ============================================================
22
+ // Constants
23
+ // ============================================================
24
+
25
+ /** Known mindmap options (key-value). */
26
+ const KNOWN_OPTIONS = new Set(['active-tag']);
27
+
28
+ // ============================================================
29
+ // Parser
30
+ // ============================================================
31
+
32
+ export function parseMindmap(
33
+ content: string,
34
+ palette?: PaletteColors
35
+ ): ParsedMindmap {
36
+ const result: ParsedMindmap = {
37
+ title: null,
38
+ titleLineNumber: null,
39
+ roots: [],
40
+ tagGroups: [],
41
+ options: {},
42
+ diagnostics: [],
43
+ error: null,
44
+ };
45
+
46
+ const fail = (line: number, message: string): ParsedMindmap => {
47
+ const diag = makeDgmoError(line, message);
48
+ result.diagnostics.push(diag);
49
+ result.error = formatDgmoError(diag);
50
+ return result;
51
+ };
52
+
53
+ const pushError = (line: number, message: string): void => {
54
+ const diag = makeDgmoError(line, message);
55
+ result.diagnostics.push(diag);
56
+ if (!result.error) result.error = formatDgmoError(diag);
57
+ };
58
+
59
+ const pushWarning = (line: number, message: string): void => {
60
+ result.diagnostics.push(makeDgmoError(line, message, 'warning'));
61
+ };
62
+
63
+ if (!content || !content.trim()) {
64
+ return fail(0, 'No content provided');
65
+ }
66
+
67
+ const lines = content.split('\n');
68
+ let contentStarted = false;
69
+ let nodeCounter = 0;
70
+
71
+ // Tag group parsing state
72
+ let currentTagGroup: TagGroup | null = null;
73
+ const aliasMap = new Map<string, string>();
74
+
75
+ // Indent stack for hierarchy tracking
76
+ const indentStack: { node: MindmapNode; indent: number }[] = [];
77
+
78
+ // Track which nodes have had a child added (for late-description warnings)
79
+ const nodesWithChildren = new Set<string>();
80
+
81
+ // Title-derived root node (if title exists on first line)
82
+ let titleRoot: MindmapNode | null = null;
83
+
84
+ for (let i = 0; i < lines.length; i++) {
85
+ const line = lines[i];
86
+ const lineNumber = i + 1;
87
+ const trimmed = line.trim();
88
+
89
+ // Skip empty lines
90
+ if (!trimmed) {
91
+ if (currentTagGroup) {
92
+ currentTagGroup = null;
93
+ }
94
+ continue;
95
+ }
96
+
97
+ // Skip comments
98
+ if (trimmed.startsWith('//')) continue;
99
+
100
+ // --- Header phase ---
101
+
102
+ if (!contentStarted) {
103
+ // Extract chart type + title from first line
104
+ const firstLine = parseFirstLine(trimmed);
105
+ if (firstLine) {
106
+ if (firstLine.chartType !== 'mindmap') {
107
+ let msg = `Expected chart type "mindmap", got "${firstLine.chartType}"`;
108
+ const hint = suggest(firstLine.chartType, ['mindmap']);
109
+ if (hint) msg += `. ${hint}`;
110
+ return fail(lineNumber, msg);
111
+ }
112
+ if (firstLine.title) {
113
+ // Title IS the root
114
+ const label = firstLine.title;
115
+ result.title = label;
116
+ result.titleLineNumber = lineNumber;
117
+
118
+ nodeCounter++;
119
+ titleRoot = {
120
+ id: `node-${nodeCounter}`,
121
+ label,
122
+ metadata: {},
123
+ children: [],
124
+ parentId: null,
125
+ lineNumber,
126
+ };
127
+ result.roots.push(titleRoot);
128
+ // Push title root onto indent stack at indent -1 so all indent-0 lines become children
129
+ indentStack.push({ node: titleRoot, indent: -1 });
130
+ }
131
+ continue;
132
+ }
133
+ }
134
+
135
+ // Tag group heading
136
+ const tagBlockMatch = matchTagBlockHeading(trimmed);
137
+ if (tagBlockMatch) {
138
+ if (contentStarted) {
139
+ pushError(lineNumber, 'Tag groups must appear before mindmap content');
140
+ continue;
141
+ }
142
+ currentTagGroup = {
143
+ name: tagBlockMatch.name,
144
+ alias: tagBlockMatch.alias,
145
+ entries: [],
146
+ lineNumber,
147
+ };
148
+ if (tagBlockMatch.alias) {
149
+ aliasMap.set(
150
+ tagBlockMatch.alias.toLowerCase(),
151
+ tagBlockMatch.name.toLowerCase()
152
+ );
153
+ }
154
+ result.tagGroups.push(currentTagGroup);
155
+ continue;
156
+ }
157
+
158
+ // Options: key-value (e.g., `active-tag Priority`)
159
+ if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
160
+ const optMatch = trimmed.match(OPTION_NOCOLON_RE);
161
+ if (optMatch) {
162
+ const key = optMatch[1].trim().toLowerCase();
163
+ if (KNOWN_OPTIONS.has(key)) {
164
+ result.options[key] = optMatch[2].trim();
165
+ continue;
166
+ }
167
+ }
168
+ // Bare keyword option: hide-descriptions
169
+ if (trimmed.toLowerCase() === 'hide-descriptions') {
170
+ result.options['hide-descriptions'] = 'true';
171
+ continue;
172
+ }
173
+ }
174
+
175
+ // Tag group entries (indented Value(color) under tag heading)
176
+ if (currentTagGroup && !contentStarted) {
177
+ const indent = measureIndent(line);
178
+ if (indent > 0) {
179
+ const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
180
+ const { label, color } = extractColor(cleanEntry, palette);
181
+ if (!color) {
182
+ pushError(
183
+ lineNumber,
184
+ `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
185
+ );
186
+ continue;
187
+ }
188
+ if (isDefault) {
189
+ currentTagGroup.defaultValue = label;
190
+ } else if (currentTagGroup.entries.length === 0) {
191
+ currentTagGroup.defaultValue = label;
192
+ }
193
+ currentTagGroup.entries.push({
194
+ value: label,
195
+ color,
196
+ lineNumber,
197
+ });
198
+ continue;
199
+ }
200
+ currentTagGroup = null; // eslint-disable-line no-useless-assignment
201
+ }
202
+
203
+ // --- Content phase ---
204
+ contentStarted = true;
205
+ currentTagGroup = null;
206
+
207
+ const indent = measureIndent(line);
208
+
209
+ // Check for indented `description: text` or `description text` metadata
210
+ if (indent > 0) {
211
+ const descResult = tryStripDescriptionKeyword(trimmed);
212
+ if (descResult.isKeyword) {
213
+ // Find parent node from indent stack
214
+ const parent = findMetadataParent(indent, indentStack);
215
+ if (parent) {
216
+ const descValue = descResult.text.trim();
217
+ if (!descValue) {
218
+ // Empty description: silently skip
219
+ continue;
220
+ }
221
+ // Check if parent already has children at this indent level
222
+ if (nodesWithChildren.has(parent.id)) {
223
+ pushWarning(
224
+ lineNumber,
225
+ `description after child nodes under "${parent.label}" — should precede children`
226
+ );
227
+ continue;
228
+ }
229
+ if (!parent.description) parent.description = [];
230
+ parent.description.push(descValue);
231
+ continue;
232
+ }
233
+ }
234
+ }
235
+
236
+ // It's a node line — possibly with pipe metadata
237
+ const node = parseNodeLine(
238
+ trimmed,
239
+ lineNumber,
240
+ palette,
241
+ ++nodeCounter,
242
+ aliasMap,
243
+ pushWarning
244
+ );
245
+ attachNode(node, indent, indentStack, result, nodesWithChildren);
246
+ }
247
+
248
+ // If no title and roots exist, infer title from first root
249
+ if (result.title === null && result.roots.length > 0) {
250
+ result.title = result.roots[0].label;
251
+ result.titleLineNumber = result.roots[0].lineNumber;
252
+ }
253
+
254
+ // Validate tag group values
255
+ if (result.tagGroups.length > 0) {
256
+ const allNodes: MindmapNode[] = [];
257
+ const collectAll = (nodes: MindmapNode[]) => {
258
+ for (const node of nodes) {
259
+ allNodes.push(node);
260
+ collectAll(node.children);
261
+ }
262
+ };
263
+ collectAll(result.roots);
264
+ validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
265
+ validateTagGroupNames(result.tagGroups, pushWarning);
266
+ }
267
+
268
+ // Check for empty mindmap
269
+ if (result.roots.length === 0 && !result.error) {
270
+ const diag = makeDgmoError(1, 'No nodes found in mindmap');
271
+ result.diagnostics.push(diag);
272
+ result.error = formatDgmoError(diag);
273
+ } else if (
274
+ titleRoot &&
275
+ titleRoot.children.length === 0 &&
276
+ result.roots.length === 1 &&
277
+ !result.error
278
+ ) {
279
+ // Title-only mindmap with no children is valid (single node)
280
+ // No error needed
281
+ }
282
+
283
+ return result;
284
+ }
285
+
286
+ // ============================================================
287
+ // Internal helpers
288
+ // ============================================================
289
+
290
+ function parseNodeLine(
291
+ trimmed: string,
292
+ lineNumber: number,
293
+ palette: PaletteColors | undefined,
294
+ counter: number,
295
+ aliasMap: Map<string, string>,
296
+ warnFn: (line: number, msg: string) => void
297
+ ): MindmapNode {
298
+ const segments = trimmed.split('|').map((s) => s.trim());
299
+ const label = segments[0];
300
+
301
+ const metadata = parsePipeMetadata(segments, aliasMap, () =>
302
+ warnFn(lineNumber, MULTIPLE_PIPE_ERROR)
303
+ );
304
+
305
+ // Extract description from pipe metadata as a dedicated field
306
+ let description: string[] | undefined;
307
+ if ('description' in metadata) {
308
+ const descVal = metadata['description'].trim();
309
+ if (descVal) {
310
+ description = [descVal];
311
+ }
312
+ delete metadata['description'];
313
+ }
314
+
315
+ // Extract collapsed flag from pipe metadata
316
+ let collapsed: boolean | undefined;
317
+ if ('collapsed' in metadata) {
318
+ collapsed = metadata['collapsed'].toLowerCase() === 'true';
319
+ delete metadata['collapsed'];
320
+ }
321
+
322
+ return {
323
+ id: `node-${counter}`,
324
+ label,
325
+ description,
326
+ metadata,
327
+ children: [],
328
+ parentId: null,
329
+ lineNumber,
330
+ collapsed,
331
+ };
332
+ }
333
+
334
+ function attachNode(
335
+ node: MindmapNode,
336
+ indent: number,
337
+ indentStack: { node: MindmapNode; indent: number }[],
338
+ result: ParsedMindmap,
339
+ nodesWithChildren: Set<string>
340
+ ): void {
341
+ // Pop stack entries with indent >= current indent
342
+ while (indentStack.length > 0) {
343
+ const top = indentStack[indentStack.length - 1];
344
+ if (top.indent < indent) break;
345
+ indentStack.pop();
346
+ }
347
+
348
+ if (indentStack.length > 0) {
349
+ const parent = indentStack[indentStack.length - 1].node;
350
+ node.parentId = parent.id;
351
+ parent.children.push(node);
352
+ nodesWithChildren.add(parent.id);
353
+ } else {
354
+ result.roots.push(node);
355
+ }
356
+
357
+ indentStack.push({ node, indent });
358
+ }
359
+
360
+ function findMetadataParent(
361
+ indent: number,
362
+ indentStack: { node: MindmapNode; indent: number }[]
363
+ ): MindmapNode | null {
364
+ for (let i = indentStack.length - 1; i >= 0; i--) {
365
+ if (indentStack[i].indent < indent) {
366
+ return indentStack[i].node;
367
+ }
368
+ }
369
+ if (indentStack.length > 0) {
370
+ return indentStack[indentStack.length - 1].node;
371
+ }
372
+ return null;
373
+ }