@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.
- package/AGENTS.md +2 -1
- package/README.md +1 -0
- package/dist/cli.cjs +142 -90
- package/dist/editor.cjs +30 -4
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +30 -4
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +25 -3
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +25 -3
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +21201 -12886
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +646 -89
- package/dist/index.d.ts +646 -89
- package/dist/index.js +21178 -12889
- 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-sitemap.md +18 -1
- package/docs/guide/chart-tech-radar.md +219 -0
- package/docs/guide/chart-wireframe.md +100 -0
- package/docs/guide/index.md +8 -0
- package/docs/guide/registry.json +1 -0
- package/docs/language-reference.md +249 -4
- package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
- package/gallery/fixtures/c4-full.dgmo +2 -2
- package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
- package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
- package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
- package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
- package/gallery/fixtures/gantt-full.dgmo +2 -2
- package/gallery/fixtures/gantt.dgmo +2 -2
- package/gallery/fixtures/infra-full.dgmo +2 -2
- package/gallery/fixtures/infra.dgmo +1 -1
- package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
- package/gallery/fixtures/sequence-tags.dgmo +2 -2
- package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
- package/gallery/fixtures/tech-radar.dgmo +36 -0
- package/gallery/fixtures/timeline.dgmo +1 -1
- package/package.json +1 -1
- package/src/boxes-and-lines/collapse.ts +21 -3
- package/src/boxes-and-lines/layout.ts +360 -42
- package/src/boxes-and-lines/parser.ts +94 -11
- package/src/boxes-and-lines/renderer.ts +371 -114
- package/src/boxes-and-lines/types.ts +2 -1
- package/src/c4/layout.ts +8 -8
- package/src/c4/parser.ts +35 -2
- package/src/c4/renderer.ts +19 -3
- package/src/c4/types.ts +1 -0
- package/src/chart.ts +14 -7
- package/src/completion.ts +253 -0
- package/src/cycle/layout.ts +732 -0
- package/src/cycle/parser.ts +352 -0
- package/src/cycle/renderer.ts +539 -0
- package/src/cycle/types.ts +77 -0
- package/src/d3.ts +240 -40
- package/src/dgmo-router.ts +15 -0
- package/src/echarts.ts +7 -4
- package/src/editor/dgmo.grammar +5 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/editor/keywords.ts +26 -0
- package/src/gantt/parser.ts +2 -8
- package/src/graph/flowchart-parser.ts +15 -21
- package/src/graph/layout.ts +73 -9
- package/src/graph/state-collapse.ts +78 -0
- package/src/graph/state-parser.ts +5 -10
- package/src/graph/state-renderer.ts +139 -34
- package/src/index.ts +78 -0
- package/src/infra/layout.ts +218 -74
- package/src/infra/parser.ts +30 -6
- package/src/infra/renderer.ts +14 -8
- package/src/infra/types.ts +10 -3
- package/src/journey-map/layout.ts +386 -0
- package/src/journey-map/parser.ts +540 -0
- package/src/journey-map/renderer.ts +1456 -0
- package/src/journey-map/types.ts +47 -0
- package/src/kanban/parser.ts +3 -10
- package/src/kanban/renderer.ts +325 -63
- package/src/mindmap/collapse.ts +88 -0
- package/src/mindmap/layout.ts +605 -0
- package/src/mindmap/parser.ts +373 -0
- package/src/mindmap/renderer.ts +544 -0
- package/src/mindmap/text-wrap.ts +217 -0
- package/src/mindmap/types.ts +55 -0
- package/src/org/parser.ts +2 -6
- package/src/render.ts +18 -21
- package/src/sequence/renderer.ts +273 -56
- package/src/sharing.ts +3 -0
- package/src/sitemap/layout.ts +56 -18
- package/src/sitemap/parser.ts +26 -17
- package/src/sitemap/renderer.ts +34 -0
- package/src/sitemap/types.ts +1 -0
- package/src/tech-radar/index.ts +14 -0
- package/src/tech-radar/interactive.ts +1058 -0
- package/src/tech-radar/layout.ts +190 -0
- package/src/tech-radar/parser.ts +385 -0
- package/src/tech-radar/renderer.ts +1159 -0
- package/src/tech-radar/shared.ts +187 -0
- package/src/tech-radar/types.ts +81 -0
- package/src/utils/description-helpers.ts +33 -0
- package/src/utils/export-container.ts +3 -2
- package/src/utils/legend-d3.ts +1 -0
- package/src/utils/legend-layout.ts +5 -3
- package/src/utils/parsing.ts +48 -7
- package/src/utils/tag-groups.ts +46 -60
- 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,352 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Cycle Diagram — Parser
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import { makeDgmoError, formatDgmoError } from '../diagnostics';
|
|
6
|
+
import {
|
|
7
|
+
measureIndent,
|
|
8
|
+
parseFirstLine,
|
|
9
|
+
parsePipeMetadata,
|
|
10
|
+
} from '../utils/parsing';
|
|
11
|
+
import type { ParsedCycle, CycleNode, CycleEdge } from './types';
|
|
12
|
+
|
|
13
|
+
// ── Edge pattern: `->`, `-label->` with optional target and pipe metadata ──
|
|
14
|
+
// Bare: `-> [Target] [| metadata]`
|
|
15
|
+
const BARE_EDGE_RE = /^->\s*(.*)?$/;
|
|
16
|
+
// Labeled: `-Label-> [Target] [| metadata]`
|
|
17
|
+
const LABELED_EDGE_RE = /^-(.+?)->\s*(.*)?$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse a `.dgmo` cycle diagram document.
|
|
21
|
+
*
|
|
22
|
+
* Syntax:
|
|
23
|
+
* ```
|
|
24
|
+
* cycle Title
|
|
25
|
+
*
|
|
26
|
+
* direction-counterclockwise
|
|
27
|
+
*
|
|
28
|
+
* NodeLabel | color: blue, span: 3
|
|
29
|
+
* Description line (indented under node)
|
|
30
|
+
* -Label-> | color: red, width: 6
|
|
31
|
+
* Edge description (indented under edge)
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function parseCycle(content: string): ParsedCycle {
|
|
35
|
+
const result: ParsedCycle = {
|
|
36
|
+
type: 'cycle',
|
|
37
|
+
title: '',
|
|
38
|
+
titleLineNumber: 0,
|
|
39
|
+
nodes: [],
|
|
40
|
+
edges: [],
|
|
41
|
+
direction: 'clockwise',
|
|
42
|
+
options: {},
|
|
43
|
+
diagnostics: [],
|
|
44
|
+
error: null,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
let headerParsed = false;
|
|
49
|
+
|
|
50
|
+
// State machine
|
|
51
|
+
type State = 'top' | 'node' | 'edge';
|
|
52
|
+
let state: State = 'top';
|
|
53
|
+
let currentNode: CycleNode | null = null;
|
|
54
|
+
let currentEdge: CycleEdge | null = null;
|
|
55
|
+
// nodeBaseIndent tracking removed — indent-based nesting not used in cycle
|
|
56
|
+
|
|
57
|
+
const fail = (line: number, message: string): ParsedCycle => {
|
|
58
|
+
const diag = makeDgmoError(line, message);
|
|
59
|
+
result.diagnostics.push(diag);
|
|
60
|
+
result.error = formatDgmoError(diag);
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const warn = (
|
|
65
|
+
line: number,
|
|
66
|
+
message: string,
|
|
67
|
+
severity: 'warning' | 'error' = 'warning'
|
|
68
|
+
): void => {
|
|
69
|
+
result.diagnostics.push(makeDgmoError(line, message, severity));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const info = (line: number, message: string): void => {
|
|
73
|
+
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function flushEdge(): void {
|
|
77
|
+
if (currentEdge) {
|
|
78
|
+
result.edges.push(currentEdge);
|
|
79
|
+
currentEdge = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function flushNode(): void {
|
|
84
|
+
flushEdge();
|
|
85
|
+
if (currentNode) {
|
|
86
|
+
result.nodes.push(currentNode);
|
|
87
|
+
currentNode = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
const lineNum = i + 1;
|
|
93
|
+
const raw = lines[i];
|
|
94
|
+
const trimmed = raw.trim();
|
|
95
|
+
|
|
96
|
+
// Skip blanks and comments
|
|
97
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
98
|
+
|
|
99
|
+
const indent = measureIndent(raw);
|
|
100
|
+
|
|
101
|
+
// ── First line: chart type declaration ──
|
|
102
|
+
if (!headerParsed) {
|
|
103
|
+
const firstLineResult = parseFirstLine(trimmed);
|
|
104
|
+
if (firstLineResult && firstLineResult.chartType === 'cycle') {
|
|
105
|
+
result.title = firstLineResult.title ?? '';
|
|
106
|
+
result.titleLineNumber = lineNum;
|
|
107
|
+
headerParsed = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
return fail(lineNum, 'Expected "cycle [Title]" as the first line.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Directive: direction-counterclockwise ──
|
|
114
|
+
if (indent === 0 && trimmed === 'direction-counterclockwise') {
|
|
115
|
+
result.direction = 'counterclockwise';
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Bare keyword: hide-descriptions ──
|
|
120
|
+
if (indent === 0 && trimmed.toLowerCase() === 'hide-descriptions') {
|
|
121
|
+
result.options['hide-descriptions'] = 'true';
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Bare keyword: circle-nodes ──
|
|
126
|
+
if (indent === 0 && trimmed.toLowerCase() === 'circle-nodes') {
|
|
127
|
+
result.options['circle-nodes'] = 'true';
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Top-level line (indent === 0): must be a node declaration ──
|
|
132
|
+
if (indent === 0) {
|
|
133
|
+
flushNode();
|
|
134
|
+
|
|
135
|
+
// Validate: node labels cannot contain -> or <-
|
|
136
|
+
if (trimmed.includes('->') || trimmed.includes('<-')) {
|
|
137
|
+
warn(
|
|
138
|
+
lineNum,
|
|
139
|
+
'Node labels cannot contain "->". Use indented lines for edges.',
|
|
140
|
+
'error'
|
|
141
|
+
);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Parse node: Label | color: blue, span: 3, description: text
|
|
146
|
+
const pipeIdx = trimmed.indexOf('|');
|
|
147
|
+
let label: string;
|
|
148
|
+
let metadata: Record<string, string> = {};
|
|
149
|
+
|
|
150
|
+
if (pipeIdx >= 0) {
|
|
151
|
+
label = trimmed.substring(0, pipeIdx).trim();
|
|
152
|
+
const segments = [label, trimmed.substring(pipeIdx + 1)];
|
|
153
|
+
metadata = parsePipeMetadata(segments);
|
|
154
|
+
} else {
|
|
155
|
+
label = trimmed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!label) {
|
|
159
|
+
warn(lineNum, 'Empty node label.');
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Extract known keys from metadata
|
|
164
|
+
const color = metadata['color'];
|
|
165
|
+
const spanStr = metadata['span'];
|
|
166
|
+
let span = 1;
|
|
167
|
+
if (spanStr !== undefined) {
|
|
168
|
+
const spanVal = parseFloat(spanStr);
|
|
169
|
+
if (isNaN(spanVal) || spanVal <= 0) {
|
|
170
|
+
warn(
|
|
171
|
+
lineNum,
|
|
172
|
+
`span must be a positive number, got "${spanStr}".`,
|
|
173
|
+
'error'
|
|
174
|
+
);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
span = spanVal;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const descFromPipe = metadata['description'];
|
|
181
|
+
const description: string[] = descFromPipe ? [descFromPipe] : [];
|
|
182
|
+
|
|
183
|
+
// Remove known keys from metadata passthrough
|
|
184
|
+
const restMeta = { ...metadata };
|
|
185
|
+
delete restMeta['color'];
|
|
186
|
+
delete restMeta['span'];
|
|
187
|
+
delete restMeta['description'];
|
|
188
|
+
|
|
189
|
+
currentNode = {
|
|
190
|
+
label,
|
|
191
|
+
lineNumber: lineNum,
|
|
192
|
+
color,
|
|
193
|
+
span,
|
|
194
|
+
description,
|
|
195
|
+
metadata: restMeta,
|
|
196
|
+
};
|
|
197
|
+
state = 'node';
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Indented lines ──
|
|
202
|
+
if (indent > 0) {
|
|
203
|
+
// Check for edge pattern: -> or -label->
|
|
204
|
+
const bareMatch = trimmed.match(BARE_EDGE_RE);
|
|
205
|
+
const labeledMatch = !bareMatch ? trimmed.match(LABELED_EDGE_RE) : null;
|
|
206
|
+
const edgeMatch = bareMatch ?? labeledMatch;
|
|
207
|
+
if (edgeMatch) {
|
|
208
|
+
// Flush any previous edge
|
|
209
|
+
flushEdge();
|
|
210
|
+
|
|
211
|
+
if (!currentNode) {
|
|
212
|
+
warn(lineNum, 'Edge line found outside of a node context.');
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const edgeLabel = bareMatch
|
|
217
|
+
? undefined
|
|
218
|
+
: labeledMatch![1]?.trim() || undefined;
|
|
219
|
+
const rest = (
|
|
220
|
+
bareMatch ? (bareMatch[1] ?? '') : (labeledMatch![2] ?? '')
|
|
221
|
+
).trim();
|
|
222
|
+
|
|
223
|
+
// Parse optional pipe metadata on the edge
|
|
224
|
+
let edgeMeta: Record<string, string> = {};
|
|
225
|
+
const edgePipeIdx = rest.indexOf('|');
|
|
226
|
+
let explicitTarget: string | undefined;
|
|
227
|
+
|
|
228
|
+
if (edgePipeIdx >= 0) {
|
|
229
|
+
explicitTarget = rest.substring(0, edgePipeIdx).trim() || undefined;
|
|
230
|
+
const segments = ['', rest.substring(edgePipeIdx + 1)];
|
|
231
|
+
edgeMeta = parsePipeMetadata(segments);
|
|
232
|
+
} else {
|
|
233
|
+
explicitTarget = rest || undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const edgeColor = edgeMeta['color'];
|
|
237
|
+
const widthStr = edgeMeta['width'];
|
|
238
|
+
const edgeWidth = widthStr ? parseFloat(widthStr) : undefined;
|
|
239
|
+
const edgeDescFromPipe = edgeMeta['description'];
|
|
240
|
+
|
|
241
|
+
// Remove known keys
|
|
242
|
+
const edgeRestMeta = { ...edgeMeta };
|
|
243
|
+
delete edgeRestMeta['color'];
|
|
244
|
+
delete edgeRestMeta['width'];
|
|
245
|
+
delete edgeRestMeta['description'];
|
|
246
|
+
|
|
247
|
+
// sourceIndex is the index of the current node (it hasn't been pushed yet)
|
|
248
|
+
const sourceIndex = result.nodes.length;
|
|
249
|
+
// targetIndex is always the next node (will be resolved post-parse)
|
|
250
|
+
const targetIndex = sourceIndex + 1;
|
|
251
|
+
|
|
252
|
+
currentEdge = {
|
|
253
|
+
sourceIndex,
|
|
254
|
+
targetIndex,
|
|
255
|
+
label: edgeLabel,
|
|
256
|
+
color: edgeColor,
|
|
257
|
+
width: edgeWidth,
|
|
258
|
+
description: edgeDescFromPipe ? [edgeDescFromPipe] : [],
|
|
259
|
+
lineNumber: lineNum,
|
|
260
|
+
metadata: edgeRestMeta,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Check explicit target for diagnostic
|
|
264
|
+
if (explicitTarget) {
|
|
265
|
+
// Store for post-parse validation
|
|
266
|
+
(
|
|
267
|
+
currentEdge as CycleEdge & { _explicitTarget?: string }
|
|
268
|
+
)._explicitTarget = explicitTarget;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
state = 'edge';
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Not an edge — must be a description line
|
|
276
|
+
if (state === 'edge' && currentEdge) {
|
|
277
|
+
// Description under an edge
|
|
278
|
+
// Handle bullet points: `- item` → `• item`
|
|
279
|
+
const descLine = trimmed.startsWith('- ')
|
|
280
|
+
? `• ${trimmed.substring(2)}`
|
|
281
|
+
: trimmed;
|
|
282
|
+
currentEdge.description.push(descLine);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (state === 'node' && currentNode) {
|
|
287
|
+
// Description under a node
|
|
288
|
+
const descLine = trimmed.startsWith('- ')
|
|
289
|
+
? `• ${trimmed.substring(2)}`
|
|
290
|
+
: trimmed;
|
|
291
|
+
currentNode.description.push(descLine);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Indented line with no context
|
|
296
|
+
warn(lineNum, `Unexpected indented line: "${trimmed}".`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Flush remaining
|
|
302
|
+
flushNode();
|
|
303
|
+
|
|
304
|
+
// ── Post-parse validation ──
|
|
305
|
+
if (result.nodes.length < 2) {
|
|
306
|
+
return fail(
|
|
307
|
+
result.titleLineNumber || 1,
|
|
308
|
+
'cycle requires at least 2 nodes.'
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Resolve edge targets and generate implicit edges ──
|
|
313
|
+
const nodeCount = result.nodes.length;
|
|
314
|
+
const edgeBySource = new Map<number, CycleEdge>();
|
|
315
|
+
for (const edge of result.edges) {
|
|
316
|
+
// Fix target index to wrap around
|
|
317
|
+
edge.targetIndex = (edge.sourceIndex + 1) % nodeCount;
|
|
318
|
+
edgeBySource.set(edge.sourceIndex, edge);
|
|
319
|
+
|
|
320
|
+
// Check explicit target diagnostic
|
|
321
|
+
const typed = edge as CycleEdge & { _explicitTarget?: string };
|
|
322
|
+
if (typed._explicitTarget) {
|
|
323
|
+
const actualTarget = result.nodes[edge.targetIndex].label;
|
|
324
|
+
if (typed._explicitTarget !== actualTarget) {
|
|
325
|
+
info(
|
|
326
|
+
edge.lineNumber!,
|
|
327
|
+
`In cycle diagrams, edges always connect to the next node ('${actualTarget}'). Explicit target '${typed._explicitTarget}' is ignored.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
delete typed._explicitTarget;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Generate implicit edges for nodes without explicit edge annotations
|
|
335
|
+
const allEdges: CycleEdge[] = [];
|
|
336
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
337
|
+
const existing = edgeBySource.get(i);
|
|
338
|
+
if (existing) {
|
|
339
|
+
allEdges.push(existing);
|
|
340
|
+
} else {
|
|
341
|
+
allEdges.push({
|
|
342
|
+
sourceIndex: i,
|
|
343
|
+
targetIndex: (i + 1) % nodeCount,
|
|
344
|
+
description: [],
|
|
345
|
+
metadata: {},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
result.edges = allEdges;
|
|
350
|
+
|
|
351
|
+
return result;
|
|
352
|
+
}
|