@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.
- package/dist/cli.cjs +92 -90
- package/dist/editor.cjs +13 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +13 -1
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +13 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +13 -1
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +4144 -940
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +318 -84
- package/dist/index.d.ts +318 -84
- package/dist/index.js +4132 -938
- package/dist/index.js.map +1 -1
- package/docs/guide/chart-mindmap.md +198 -0
- package/docs/guide/chart-sequence.md +23 -1
- package/docs/guide/chart-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/language-reference.md +137 -2
- package/package.json +1 -1
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +51 -9
- package/src/boxes-and-lines/parser.ts +8 -1
- package/src/boxes-and-lines/renderer.ts +121 -23
- package/src/boxes-and-lines/types.ts +1 -0
- package/src/completion.ts +26 -0
- package/src/d3.ts +153 -32
- package/src/dgmo-router.ts +6 -0
- package/src/editor/keywords.ts +12 -0
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +28 -0
- package/src/kanban/renderer.ts +303 -57
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +379 -0
- package/src/mindmap/renderer.ts +543 -0
- package/src/mindmap/text-wrap.ts +207 -0
- package/src/mindmap/types.ts +55 -0
- package/src/render.ts +18 -21
- package/src/sequence/renderer.ts +129 -18
- package/src/sharing.ts +2 -0
- package/src/sitemap/layout.ts +35 -12
- package/src/utils/export-container.ts +3 -2
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +2 -2
- package/src/utils/parsing.ts +2 -0
- package/src/wireframe/layout.ts +460 -0
- package/src/wireframe/parser.ts +956 -0
- package/src/wireframe/renderer.ts +1293 -0
- 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
|
+
}
|