@diagrammo/dgmo 0.7.3 → 0.8.0

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 (62) hide show
  1. package/AGENTS.md +15 -20
  2. package/README.md +56 -58
  3. package/dist/cli.cjs +188 -181
  4. package/dist/index.cjs +3506 -1057
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +196 -43
  7. package/dist/index.d.ts +196 -43
  8. package/dist/index.js +3493 -1057
  9. package/dist/index.js.map +1 -1
  10. package/docs/language-reference.md +629 -289
  11. package/package.json +1 -1
  12. package/src/c4/layout.ts +6 -9
  13. package/src/c4/parser.ts +189 -83
  14. package/src/c4/renderer.ts +8 -9
  15. package/src/chart.ts +296 -83
  16. package/src/class/parser.ts +54 -37
  17. package/src/class/renderer.ts +8 -8
  18. package/src/cli.ts +8 -8
  19. package/src/colors.ts +4 -1
  20. package/src/completion.ts +757 -10
  21. package/src/d3.ts +310 -73
  22. package/src/dgmo-router.ts +63 -8
  23. package/src/echarts.ts +726 -231
  24. package/src/er/parser.ts +94 -76
  25. package/src/er/renderer.ts +6 -5
  26. package/src/gantt/parser.ts +144 -69
  27. package/src/gantt/renderer.ts +50 -14
  28. package/src/gantt/types.ts +3 -3
  29. package/src/graph/flowchart-parser.ts +97 -37
  30. package/src/graph/flowchart-renderer.ts +4 -3
  31. package/src/graph/state-parser.ts +50 -31
  32. package/src/graph/state-renderer.ts +4 -3
  33. package/src/index.ts +14 -5
  34. package/src/infra/compute.ts +1 -0
  35. package/src/infra/layout.ts +3 -0
  36. package/src/infra/parser.ts +291 -92
  37. package/src/infra/renderer.ts +172 -30
  38. package/src/infra/types.ts +5 -0
  39. package/src/initiative-status/layout.ts +1 -1
  40. package/src/initiative-status/parser.ts +121 -47
  41. package/src/initiative-status/renderer.ts +42 -23
  42. package/src/initiative-status/types.ts +10 -2
  43. package/src/kanban/parser.ts +60 -37
  44. package/src/kanban/renderer.ts +2 -2
  45. package/src/kanban/types.ts +1 -0
  46. package/src/org/layout.ts +9 -9
  47. package/src/org/parser.ts +39 -40
  48. package/src/org/renderer.ts +5 -6
  49. package/src/org/resolver.ts +26 -19
  50. package/src/render.ts +1 -1
  51. package/src/sequence/parser.ts +304 -95
  52. package/src/sequence/renderer.ts +9 -9
  53. package/src/sitemap/layout.ts +3 -4
  54. package/src/sitemap/parser.ts +57 -49
  55. package/src/sitemap/renderer.ts +6 -7
  56. package/src/utils/arrows.ts +25 -6
  57. package/src/utils/duration.ts +43 -7
  58. package/src/utils/legend-constants.ts +26 -0
  59. package/src/utils/legend-svg.ts +167 -0
  60. package/src/utils/parsing.ts +247 -7
  61. package/src/utils/tag-groups.ts +160 -15
  62. package/src/utils/title-constants.ts +9 -0
@@ -1,11 +1,22 @@
1
1
  import { resolveColor } from '../colors';
2
2
  import type { PaletteColors } from '../palettes';
3
3
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
4
- import { measureIndent, extractColor } from '../utils/parsing';
4
+ import {
5
+ measureIndent,
6
+ extractColor,
7
+ normalizeDirection,
8
+ inferArrowColor,
9
+ parseFirstLine,
10
+ OPTION_NOCOLON_RE,
11
+ GROUP_HASH_RE,
12
+ DOUBLE_HASH_RE,
13
+ ALL_CHART_TYPES,
14
+ } from '../utils/parsing';
5
15
  import type {
6
16
  ParsedGraph,
7
17
  GraphNode,
8
18
  GraphEdge,
19
+ GraphGroup,
9
20
  GraphShape,
10
21
  GraphDirection,
11
22
  } from './types';
@@ -177,22 +188,24 @@ function parseArrowToken(token: string, palette?: PaletteColors): ArrowInfo {
177
188
  // Color-only: -(color)->
178
189
  const colorOnly = token.match(/^-\(([^)]+)\)->$/);
179
190
  if (colorOnly) {
180
- return { color: resolveColor(colorOnly[1].trim(), palette) };
191
+ return { color: resolveColor(colorOnly[1].trim(), palette) ?? undefined };
181
192
  }
182
193
  // -label(color)-> or -label->
183
194
  const m = token.match(/^-(.+?)(?:\(([^)]+)\))?->$/);
184
195
  if (m) {
185
196
  const label = m[1]?.trim() || undefined;
186
- const color = m[2] ? resolveColor(m[2].trim(), palette) : undefined;
197
+ let color = m[2] ? resolveColor(m[2].trim(), palette) ?? undefined : undefined;
198
+ if (label && !color) {
199
+ color = inferArrowColor(label);
200
+ }
187
201
  return { label, color };
188
202
  }
189
203
  return {};
190
204
  }
191
205
 
192
206
  // ============================================================
193
- // Legacy group heading (deprecated — emit error)
207
+ // Group heading support
194
208
  // ============================================================
195
- const LEGACY_GROUP_RE = /^##\s+/;
196
209
 
197
210
  // ============================================================
198
211
  // Main parser
@@ -223,6 +236,12 @@ export function parseFlowchart(
223
236
  const nodeMap = new Map<string, GraphNode>();
224
237
  const indentStack: { nodeId: string; indent: number }[] = [];
225
238
  let contentStarted = false;
239
+ let firstLineParsed = false;
240
+
241
+ // Group support
242
+ let currentGroup: GraphGroup | null = null;
243
+ let groupIndent = -1;
244
+ const groups: GraphGroup[] = [];
226
245
 
227
246
  function getOrCreateNode(ref: NodeRef, lineNumber: number): GraphNode {
228
247
  const existing = nodeMap.get(ref.id);
@@ -234,10 +253,15 @@ export function parseFlowchart(
234
253
  shape: ref.shape,
235
254
  lineNumber,
236
255
  ...(ref.color && { color: ref.color }),
256
+ ...(currentGroup && { group: currentGroup.id }),
237
257
  };
238
258
  nodeMap.set(ref.id, node);
239
259
  result.nodes.push(node);
240
260
 
261
+ if (currentGroup && !currentGroup.nodeIds.includes(ref.id)) {
262
+ currentGroup.nodeIds.push(ref.id);
263
+ }
264
+
241
265
  return node;
242
266
  }
243
267
 
@@ -374,54 +398,89 @@ export function parseFlowchart(
374
398
  // Skip comments
375
399
  if (trimmed.startsWith('//')) continue;
376
400
 
377
- // Legacy ## group headings no longer supported
378
- if (LEGACY_GROUP_RE.test(trimmed)) {
379
- result.diagnostics.push(
380
- makeDgmoError(lineNumber, '## group syntax is not supported in flowcharts. Remove the ## line.', 'error')
381
- );
382
- continue;
383
- }
384
-
385
- // Metadata directives (before content)
386
- if (!contentStarted && trimmed.includes(':') && !trimmed.includes('->')) {
387
- const colonIdx = trimmed.indexOf(':');
388
- const key = trimmed.substring(0, colonIdx).trim().toLowerCase();
389
- const value = trimmed.substring(colonIdx + 1).trim();
390
-
391
- if (key === 'chart') {
392
- if (value.toLowerCase() !== 'flowchart') {
393
- const allTypes = ['flowchart', 'sequence', 'class', 'er', 'org', 'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline', 'arc', 'slope'];
394
- let msg = `Expected chart type "flowchart", got "${value}"`;
395
- const hint = suggest(value.toLowerCase(), allTypes);
401
+ // First line: try parseFirstLine for `flowchart [Title]`
402
+ if (!firstLineParsed && !contentStarted) {
403
+ const firstLineResult = parseFirstLine(trimmed);
404
+ if (firstLineResult) {
405
+ firstLineParsed = true;
406
+ if (firstLineResult.chartType !== 'flowchart') {
407
+ const allTypes = Array.from(ALL_CHART_TYPES);
408
+ let msg = `Expected chart type "flowchart", got "${firstLineResult.chartType}"`;
409
+ const hint = suggest(firstLineResult.chartType, allTypes);
396
410
  if (hint) msg += `. ${hint}`;
397
411
  return fail(lineNumber, msg);
398
412
  }
413
+ if (firstLineResult.title) {
414
+ result.title = firstLineResult.title;
415
+ result.titleLineNumber = lineNumber;
416
+ }
399
417
  continue;
400
418
  }
419
+ }
401
420
 
402
- if (key === 'title') {
403
- result.title = value;
404
- result.titleLineNumber = lineNumber;
405
- continue;
406
- }
421
+ // ## group headings — emit helpful error
422
+ if (DOUBLE_HASH_RE.test(trimmed)) {
423
+ result.diagnostics.push(
424
+ makeDgmoError(lineNumber, 'Use `#` for groups \u2014 nesting is done with indentation.', 'error')
425
+ );
426
+ continue;
427
+ }
428
+
429
+ // # GroupName — alternate group notation
430
+ const hashGroupMatch = trimmed.match(GROUP_HASH_RE);
431
+ if (hashGroupMatch) {
432
+ const { label, color } = extractColor(hashGroupMatch[1].trim(), palette);
433
+ currentGroup = {
434
+ id: `group:${label.toLowerCase()}`,
435
+ label,
436
+ nodeIds: [],
437
+ lineNumber,
438
+ ...(color && { color }),
439
+ };
440
+ groupIndent = indent;
441
+ groups.push(currentGroup);
442
+ continue;
443
+ }
407
444
 
408
- if (key === 'direction') {
409
- const dir = value.toUpperCase() as GraphDirection;
410
- if (dir === 'TB' || dir === 'LR') {
411
- result.direction = dir;
445
+ // Options (space-separated, before content)
446
+ if (!contentStarted) {
447
+ const optMatch = trimmed.match(OPTION_NOCOLON_RE);
448
+ if (optMatch && !trimmed.includes('->')) {
449
+ const key = optMatch[1].toLowerCase();
450
+ const value = optMatch[2].trim();
451
+
452
+ if (key === 'direction' || key === 'orientation') {
453
+ const dir = normalizeDirection(value);
454
+ if (dir) {
455
+ result.direction = dir;
456
+ }
457
+ continue;
412
458
  }
459
+
460
+ // Boolean: no-color = color off
461
+ if (key === 'no-color') {
462
+ result.options['color'] = 'off';
463
+ continue;
464
+ }
465
+
466
+ // Store other options (e.g., color off)
467
+ result.options[key] = value;
413
468
  continue;
414
469
  }
470
+ }
415
471
 
416
- // Store other options (e.g., color: off)
417
- result.options[key] = value;
418
- continue;
472
+ // Close current group when indent returns to or below the group level
473
+ if (currentGroup && indent <= groupIndent) {
474
+ currentGroup = null;
475
+ groupIndent = -1;
419
476
  }
420
477
 
421
478
  // Content line (nodes and edges)
422
479
  processContentLine(trimmed, lineNumber, indent);
423
480
  }
424
481
 
482
+ if (groups.length > 0) result.groups = groups;
483
+
425
484
  // Validation: no nodes found
426
485
  if (result.nodes.length === 0 && !result.error) {
427
486
  const diag = makeDgmoError(1, 'No nodes found. Add flowchart content with shape syntax like [Process] or (Start).');
@@ -497,7 +556,8 @@ export function extractSymbols(docText: string): DiagramSymbols {
497
556
  let inMetadata = true;
498
557
  for (const rawLine of docText.split('\n')) {
499
558
  const line = rawLine.trim();
500
- if (inMetadata && /^[a-z-]+\s*:/i.test(line)) continue;
559
+ // Skip old-style colon metadata and new-style space-separated options
560
+ if (inMetadata && (/^[a-z-]+\s*:/i.test(line) || /^[a-z-]+\s+\S/i.test(line))) continue;
501
561
  inMetadata = false;
502
562
  if (line.length === 0 || /^\s/.test(rawLine)) continue;
503
563
  const m = NODE_ID_RE.exec(line);
@@ -11,6 +11,7 @@ import type { ParsedGraph, GraphShape } from './types';
11
11
  import type { LayoutResult, LayoutNode } from './layout';
12
12
  import { parseFlowchart } from './flowchart-parser';
13
13
  import { layoutGraph } from './layout';
14
+ import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y } from '../utils/title-constants';
14
15
 
15
16
  // ============================================================
16
17
  // Constants
@@ -301,11 +302,11 @@ export function renderFlowchart(
301
302
  .append('text')
302
303
  .attr('class', 'chart-title')
303
304
  .attr('x', width / 2)
304
- .attr('y', 30)
305
+ .attr('y', TITLE_Y)
305
306
  .attr('text-anchor', 'middle')
306
307
  .attr('fill', palette.text)
307
- .attr('font-size', '20px')
308
- .attr('font-weight', '700')
308
+ .attr('font-size', TITLE_FONT_SIZE)
309
+ .attr('font-weight', TITLE_FONT_WEIGHT)
309
310
  .style('cursor', onClickItem && graph.titleLineNumber ? 'pointer' : 'default')
310
311
  .text(graph.title);
311
312
 
@@ -1,7 +1,14 @@
1
1
  import { resolveColor } from '../colors';
2
2
  import type { PaletteColors } from '../palettes';
3
3
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
4
- import { measureIndent, extractColor } from '../utils/parsing';
4
+ import {
5
+ measureIndent,
6
+ extractColor,
7
+ normalizeDirection,
8
+ parseFirstLine,
9
+ OPTION_NOCOLON_RE,
10
+ ALL_CHART_TYPES,
11
+ } from '../utils/parsing';
5
12
  import type {
6
13
  ParsedGraph,
7
14
  GraphNode,
@@ -95,11 +102,11 @@ interface ArrowInfo {
95
102
  function parseArrowToken(token: string, palette?: PaletteColors): ArrowInfo {
96
103
  if (token === '->') return {};
97
104
  const colorOnly = token.match(/^-\(([^)]+)\)->$/);
98
- if (colorOnly) return { color: resolveColor(colorOnly[1].trim(), palette) };
105
+ if (colorOnly) return { color: resolveColor(colorOnly[1].trim(), palette) ?? undefined };
99
106
  const m = token.match(/^-(.+?)(?:\(([^)]+)\))?->$/);
100
107
  if (m) {
101
108
  const label = m[1]?.trim() || undefined;
102
- const color = m[2] ? resolveColor(m[2].trim(), palette) : undefined;
109
+ const color = m[2] ? resolveColor(m[2].trim(), palette) ?? undefined : undefined;
103
110
  return { label, color };
104
111
  }
105
112
  return {};
@@ -168,6 +175,7 @@ export function parseState(
168
175
  let groupIndent = -1;
169
176
  const groups: GraphGroup[] = [];
170
177
  let contentStarted = false;
178
+ let firstLineParsed = false;
171
179
 
172
180
  function getOrCreateNode(ref: NodeRef, lineNumber: number): GraphNode {
173
181
  const existing = nodeMap.get(ref.id);
@@ -217,6 +225,26 @@ export function parseState(
217
225
  if (!trimmed) continue;
218
226
  if (trimmed.startsWith('//')) continue;
219
227
 
228
+ // First line: try parseFirstLine for `state [Title]`
229
+ if (!firstLineParsed && !contentStarted) {
230
+ const firstLineResult = parseFirstLine(trimmed);
231
+ if (firstLineResult) {
232
+ firstLineParsed = true;
233
+ if (firstLineResult.chartType !== 'state') {
234
+ const allTypes = Array.from(ALL_CHART_TYPES);
235
+ let msg = `Expected chart type "state", got "${firstLineResult.chartType}"`;
236
+ const hint = suggest(firstLineResult.chartType, allTypes);
237
+ if (hint) msg += `. ${hint}`;
238
+ return fail(lineNumber, msg);
239
+ }
240
+ if (firstLineResult.title) {
241
+ result.title = firstLineResult.title;
242
+ result.titleLineNumber = lineNumber;
243
+ }
244
+ continue;
245
+ }
246
+ }
247
+
220
248
  // Group brackets: [Name] or [Name](color)
221
249
  const groupMatch = trimmed.match(GROUP_BRACKET_RE);
222
250
  if (groupMatch && groupMatch[1].trim() !== '*') {
@@ -238,39 +266,30 @@ export function parseState(
238
266
  continue;
239
267
  }
240
268
 
241
- // Metadata directives (before content)
242
- if (!contentStarted && trimmed.includes(':') && !trimmed.includes('->')) {
243
- const colonIdx = trimmed.indexOf(':');
244
- const key = trimmed.substring(0, colonIdx).trim().toLowerCase();
245
- const value = trimmed.substring(colonIdx + 1).trim();
246
-
247
- if (key === 'chart') {
248
- if (value.toLowerCase() !== 'state') {
249
- const allTypes = ['state', 'flowchart', 'sequence', 'class', 'er', 'org', 'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline', 'arc', 'slope'];
250
- let msg = `Expected chart type "state", got "${value}"`;
251
- const hint = suggest(value.toLowerCase(), allTypes);
252
- if (hint) msg += `. ${hint}`;
253
- return fail(lineNumber, msg);
269
+ // Options (space-separated, before content)
270
+ if (!contentStarted) {
271
+ const optMatch = trimmed.match(OPTION_NOCOLON_RE);
272
+ if (optMatch && !trimmed.includes('->')) {
273
+ const key = optMatch[1].toLowerCase();
274
+ const value = optMatch[2].trim();
275
+
276
+ if (key === 'direction' || key === 'orientation') {
277
+ const dir = normalizeDirection(value);
278
+ if (dir) {
279
+ result.direction = dir;
280
+ }
281
+ continue;
254
282
  }
255
- continue;
256
- }
257
283
 
258
- if (key === 'title') {
259
- result.title = value;
260
- result.titleLineNumber = lineNumber;
261
- continue;
262
- }
263
-
264
- if (key === 'direction') {
265
- const dir = value.toUpperCase() as GraphDirection;
266
- if (dir === 'TB' || dir === 'LR') {
267
- result.direction = dir;
284
+ // Boolean: no-color = color off
285
+ if (key === 'no-color') {
286
+ result.options['color'] = 'off';
287
+ continue;
268
288
  }
289
+
290
+ result.options[key] = value;
269
291
  continue;
270
292
  }
271
-
272
- result.options[key] = value;
273
- continue;
274
293
  }
275
294
 
276
295
  // Content line — nodes and edges
@@ -11,6 +11,7 @@ import type { ParsedGraph } from './types';
11
11
  import type { LayoutResult, LayoutNode } from './layout';
12
12
  import { parseState } from './state-parser';
13
13
  import { layoutGraph } from './layout';
14
+ import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y } from '../utils/title-constants';
14
15
 
15
16
  // ============================================================
16
17
  // Constants
@@ -154,11 +155,11 @@ export function renderState(
154
155
  .append('text')
155
156
  .attr('class', 'chart-title')
156
157
  .attr('x', width / 2)
157
- .attr('y', 30)
158
+ .attr('y', TITLE_Y)
158
159
  .attr('text-anchor', 'middle')
159
160
  .attr('fill', palette.text)
160
- .attr('font-size', '20px')
161
- .attr('font-weight', '700')
161
+ .attr('font-size', TITLE_FONT_SIZE)
162
+ .attr('font-weight', TITLE_FONT_WEIGHT)
162
163
  .style('cursor', onClickItem && graph.titleLineNumber ? 'pointer' : 'default')
163
164
  .text(graph.title);
164
165
 
package/src/index.ts CHANGED
@@ -139,8 +139,6 @@ export { parseOrg } from './org/parser';
139
139
  export type {
140
140
  ParsedOrg,
141
141
  OrgNode,
142
- OrgTagGroup,
143
- OrgTagEntry,
144
142
  } from './org/parser';
145
143
 
146
144
  export { layoutOrg } from './org/layout';
@@ -259,7 +257,7 @@ export type { InfraRole } from './infra/roles';
259
257
  export { layoutInfra } from './infra/layout';
260
258
  export type { InfraLayoutResult, InfraLayoutNode, InfraLayoutEdge, InfraLayoutGroup } from './infra/layout';
261
259
  export { renderInfra, parseAndLayoutInfra, computeInfraLegendGroups } from './infra/renderer';
262
- export type { InfraLegendGroup } from './infra/renderer';
260
+ export type { InfraLegendGroup, InfraPlaybackState } from './infra/renderer';
263
261
  export type { CollapsedSitemapResult } from './sitemap/collapse';
264
262
 
265
263
  // ── Gantt Chart ───────────────────────────────────────────
@@ -306,7 +304,10 @@ export { renderFlowchart, renderFlowchartForExport } from './graph/flowchart-ren
306
304
  // Config Builders (produce framework-specific config objects)
307
305
  // ============================================================
308
306
 
309
- export { buildExtendedChartOption, buildSimpleChartOption, renderExtendedChartForExport } from './echarts';
307
+ export { buildExtendedChartOption, buildSimpleChartOption, renderExtendedChartForExport, getExtendedChartLegendGroups, getSimpleChartLegendGroups, computeScatterLabelGraphics } from './echarts';
308
+ export type { ScatterLabelPoint } from './echarts';
309
+ export { renderLegendSvg, type LegendGroupData } from './utils/legend-svg';
310
+ export { LEGEND_HEIGHT } from './utils/legend-constants';
310
311
  export { buildMermaidQuadrant } from './dgmo-mermaid';
311
312
 
312
313
  // ============================================================
@@ -395,8 +396,16 @@ export type {
395
396
  export {
396
397
  registerExtractor,
397
398
  extractDiagramSymbols,
399
+ COMPLETION_REGISTRY,
400
+ CHART_TYPES,
401
+ METADATA_KEY_SET,
402
+ ENTITY_TYPES,
403
+ PIPE_METADATA,
404
+ extractTagDeclarations,
398
405
  } from './completion';
399
- export type { DiagramSymbols, ExtractFn } from './completion';
406
+ export type { DiagramSymbols, ExtractFn, DirectiveSpec, DirectiveValueSpec, PipeKeySpec } from './completion';
407
+
408
+ export { parseFirstLine, ALL_CHART_TYPES } from './utils/parsing';
400
409
 
401
410
  // ============================================================
402
411
  // Branding
@@ -1141,6 +1141,7 @@ export function computeInfra(
1141
1141
  sourceId: edge.sourceId,
1142
1142
  targetId: edge.targetId,
1143
1143
  label: edge.label,
1144
+ async: edge.async,
1144
1145
  computedRps: rps,
1145
1146
  split: resolvedSplit,
1146
1147
  fanout: edge.fanout,
@@ -47,6 +47,7 @@ export interface InfraLayoutEdge {
47
47
  sourceId: string;
48
48
  targetId: string;
49
49
  label: string;
50
+ async: boolean;
50
51
  computedRps: number;
51
52
  split: number;
52
53
  fanout: number | null;
@@ -575,6 +576,7 @@ export function layoutInfra(computed: ComputedInfraModel, expandedNodeIds?: Set<
575
576
  sourceId: edge.sourceId,
576
577
  targetId: edge.targetId,
577
578
  label: edge.label,
579
+ async: edge.async,
578
580
  computedRps: edge.computedRps,
579
581
  split: edge.split,
580
582
  fanout: edge.fanout,
@@ -587,6 +589,7 @@ export function layoutInfra(computed: ComputedInfraModel, expandedNodeIds?: Set<
587
589
  sourceId: edge.sourceId,
588
590
  targetId: edge.targetId,
589
591
  label: edge.label,
592
+ async: edge.async,
590
593
  computedRps: edge.computedRps,
591
594
  split: edge.split,
592
595
  fanout: edge.fanout,