@diagrammo/dgmo 0.8.21 → 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 (93) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -0
  3. package/dist/cli.cjs +143 -93
  4. package/dist/editor.cjs +17 -3
  5. package/dist/editor.cjs.map +1 -1
  6. package/dist/editor.js +17 -3
  7. package/dist/editor.js.map +1 -1
  8. package/dist/highlight.cjs +12 -2
  9. package/dist/highlight.cjs.map +1 -1
  10. package/dist/highlight.js +12 -2
  11. package/dist/highlight.js.map +1 -1
  12. package/dist/index.cjs +19997 -14886
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +331 -8
  15. package/dist/index.d.ts +331 -8
  16. package/dist/index.js +19984 -14889
  17. package/dist/index.js.map +1 -1
  18. package/docs/guide/chart-sitemap.md +18 -1
  19. package/docs/guide/chart-tech-radar.md +219 -0
  20. package/docs/guide/registry.json +1 -0
  21. package/docs/language-reference.md +116 -6
  22. package/gallery/fixtures/boxes-and-lines.dgmo +10 -3
  23. package/gallery/fixtures/c4-full.dgmo +2 -2
  24. package/gallery/fixtures/cycle/ooda-loop.dgmo +25 -0
  25. package/gallery/fixtures/cycle/pdca-circle-nodes.dgmo +12 -0
  26. package/gallery/fixtures/cycle/pdca-minimal.dgmo +6 -0
  27. package/gallery/fixtures/cycle/sprint-cycle-span.dgmo +17 -0
  28. package/gallery/fixtures/gantt-full.dgmo +2 -2
  29. package/gallery/fixtures/gantt.dgmo +2 -2
  30. package/gallery/fixtures/infra-full.dgmo +2 -2
  31. package/gallery/fixtures/infra.dgmo +1 -1
  32. package/gallery/fixtures/sequence-tags-protocols.dgmo +2 -2
  33. package/gallery/fixtures/sequence-tags.dgmo +2 -2
  34. package/gallery/fixtures/tech-radar-dense.dgmo +77 -0
  35. package/gallery/fixtures/tech-radar.dgmo +36 -0
  36. package/gallery/fixtures/timeline.dgmo +1 -1
  37. package/package.json +1 -1
  38. package/src/boxes-and-lines/layout.ts +309 -33
  39. package/src/boxes-and-lines/parser.ts +86 -10
  40. package/src/boxes-and-lines/renderer.ts +250 -91
  41. package/src/boxes-and-lines/types.ts +1 -1
  42. package/src/c4/layout.ts +8 -8
  43. package/src/c4/parser.ts +35 -2
  44. package/src/c4/renderer.ts +19 -3
  45. package/src/c4/types.ts +1 -0
  46. package/src/chart.ts +14 -7
  47. package/src/completion.ts +227 -0
  48. package/src/cycle/layout.ts +732 -0
  49. package/src/cycle/parser.ts +352 -0
  50. package/src/cycle/renderer.ts +539 -0
  51. package/src/cycle/types.ts +77 -0
  52. package/src/d3.ts +87 -8
  53. package/src/dgmo-router.ts +9 -0
  54. package/src/echarts.ts +7 -4
  55. package/src/editor/dgmo.grammar +5 -1
  56. package/src/editor/dgmo.grammar.js +1 -1
  57. package/src/editor/keywords.ts +14 -0
  58. package/src/gantt/parser.ts +2 -8
  59. package/src/graph/flowchart-parser.ts +15 -21
  60. package/src/graph/state-parser.ts +5 -10
  61. package/src/index.ts +50 -0
  62. package/src/infra/layout.ts +218 -74
  63. package/src/infra/parser.ts +30 -6
  64. package/src/infra/renderer.ts +14 -8
  65. package/src/infra/types.ts +10 -3
  66. package/src/journey-map/layout.ts +386 -0
  67. package/src/journey-map/parser.ts +540 -0
  68. package/src/journey-map/renderer.ts +1456 -0
  69. package/src/journey-map/types.ts +47 -0
  70. package/src/kanban/parser.ts +3 -10
  71. package/src/kanban/renderer.ts +31 -15
  72. package/src/mindmap/parser.ts +12 -18
  73. package/src/mindmap/renderer.ts +14 -13
  74. package/src/mindmap/text-wrap.ts +22 -12
  75. package/src/mindmap/types.ts +2 -2
  76. package/src/org/parser.ts +2 -6
  77. package/src/sequence/renderer.ts +144 -38
  78. package/src/sharing.ts +1 -0
  79. package/src/sitemap/layout.ts +21 -6
  80. package/src/sitemap/parser.ts +26 -17
  81. package/src/sitemap/renderer.ts +34 -0
  82. package/src/sitemap/types.ts +1 -0
  83. package/src/tech-radar/index.ts +14 -0
  84. package/src/tech-radar/interactive.ts +1058 -0
  85. package/src/tech-radar/layout.ts +190 -0
  86. package/src/tech-radar/parser.ts +385 -0
  87. package/src/tech-radar/renderer.ts +1159 -0
  88. package/src/tech-radar/shared.ts +187 -0
  89. package/src/tech-radar/types.ts +81 -0
  90. package/src/utils/description-helpers.ts +33 -0
  91. package/src/utils/legend-layout.ts +3 -1
  92. package/src/utils/parsing.ts +46 -7
  93. package/src/utils/tag-groups.ts +46 -60
@@ -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
+ }