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