@diagrammo/dgmo 0.8.3 → 0.8.5

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 (122) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +452 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +188 -185
  9. package/dist/editor.cjs +338 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +307 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/highlight.cjs +560 -0
  16. package/dist/highlight.cjs.map +1 -0
  17. package/dist/highlight.d.cts +32 -0
  18. package/dist/highlight.d.ts +32 -0
  19. package/dist/highlight.js +530 -0
  20. package/dist/highlight.js.map +1 -0
  21. package/dist/index.cjs +3467 -1078
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +22 -1
  24. package/dist/index.d.ts +22 -1
  25. package/dist/index.js +3466 -1078
  26. package/dist/index.js.map +1 -1
  27. package/docs/language-reference.md +46 -37
  28. package/gallery/fixtures/arc.dgmo +18 -0
  29. package/gallery/fixtures/area.dgmo +19 -0
  30. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  31. package/gallery/fixtures/bar.dgmo +10 -0
  32. package/gallery/fixtures/c4-full.dgmo +52 -0
  33. package/gallery/fixtures/c4.dgmo +17 -0
  34. package/gallery/fixtures/chord.dgmo +12 -0
  35. package/gallery/fixtures/class-basic.dgmo +14 -0
  36. package/gallery/fixtures/class-full.dgmo +43 -0
  37. package/gallery/fixtures/doughnut.dgmo +8 -0
  38. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  39. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  40. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  41. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  42. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  43. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  44. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  45. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  46. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  47. package/gallery/fixtures/function.dgmo +8 -0
  48. package/gallery/fixtures/funnel.dgmo +7 -0
  49. package/gallery/fixtures/gantt-full.dgmo +49 -0
  50. package/gallery/fixtures/gantt.dgmo +42 -0
  51. package/gallery/fixtures/heatmap.dgmo +8 -0
  52. package/gallery/fixtures/infra-full.dgmo +78 -0
  53. package/gallery/fixtures/infra-overload.dgmo +25 -0
  54. package/gallery/fixtures/infra.dgmo +47 -0
  55. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  56. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  57. package/gallery/fixtures/initiative-status.dgmo +9 -0
  58. package/gallery/fixtures/line.dgmo +19 -0
  59. package/gallery/fixtures/multi-line.dgmo +11 -0
  60. package/gallery/fixtures/org-basic.dgmo +16 -0
  61. package/gallery/fixtures/org-full.dgmo +69 -0
  62. package/gallery/fixtures/org-teams.dgmo +25 -0
  63. package/gallery/fixtures/pie.dgmo +9 -0
  64. package/gallery/fixtures/polar-area.dgmo +8 -0
  65. package/gallery/fixtures/quadrant.dgmo +18 -0
  66. package/gallery/fixtures/radar.dgmo +8 -0
  67. package/gallery/fixtures/sankey.dgmo +31 -0
  68. package/gallery/fixtures/scatter.dgmo +21 -0
  69. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  70. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  71. package/gallery/fixtures/sequence.dgmo +35 -0
  72. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  73. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  74. package/gallery/fixtures/slope.dgmo +9 -0
  75. package/gallery/fixtures/spr-eras.dgmo +62 -0
  76. package/gallery/fixtures/state.dgmo +30 -0
  77. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  78. package/gallery/fixtures/timeline.dgmo +32 -0
  79. package/gallery/fixtures/venn.dgmo +10 -0
  80. package/gallery/fixtures/wordcloud.dgmo +24 -0
  81. package/package.json +71 -2
  82. package/src/c4/layout.ts +372 -90
  83. package/src/c4/parser.ts +100 -55
  84. package/src/chart.ts +91 -28
  85. package/src/class/parser.ts +41 -12
  86. package/src/cli.ts +211 -62
  87. package/src/completion.ts +378 -183
  88. package/src/d3.ts +1044 -303
  89. package/src/dgmo-mermaid.ts +16 -13
  90. package/src/dgmo-router.ts +69 -23
  91. package/src/echarts.ts +646 -153
  92. package/src/editor/dgmo.grammar +69 -0
  93. package/src/editor/dgmo.grammar.d.ts +2 -0
  94. package/src/editor/dgmo.grammar.js +18 -0
  95. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  96. package/src/editor/dgmo.grammar.terms.js +35 -0
  97. package/src/editor/highlight-api.ts +444 -0
  98. package/src/editor/highlight.ts +36 -0
  99. package/src/editor/index.ts +28 -0
  100. package/src/editor/keywords.ts +222 -0
  101. package/src/editor/tokens.ts +30 -0
  102. package/src/er/parser.ts +48 -14
  103. package/src/er/renderer.ts +112 -53
  104. package/src/gantt/calculator.ts +91 -29
  105. package/src/gantt/parser.ts +197 -71
  106. package/src/gantt/renderer.ts +1120 -350
  107. package/src/graph/flowchart-parser.ts +46 -25
  108. package/src/graph/state-parser.ts +47 -17
  109. package/src/index.ts +96 -31
  110. package/src/infra/parser.ts +157 -53
  111. package/src/infra/renderer.ts +723 -271
  112. package/src/initiative-status/parser.ts +138 -44
  113. package/src/kanban/parser.ts +25 -14
  114. package/src/org/layout.ts +111 -44
  115. package/src/org/parser.ts +69 -22
  116. package/src/palettes/index.ts +3 -2
  117. package/src/sequence/parser.ts +193 -61
  118. package/src/sitemap/parser.ts +65 -29
  119. package/src/utils/arrows.ts +2 -22
  120. package/src/utils/duration.ts +39 -21
  121. package/src/utils/legend-constants.ts +0 -2
  122. package/src/utils/parsing.ts +75 -31
@@ -21,12 +21,21 @@ import {
21
21
  LEGEND_GROUP_GAP,
22
22
  measureLegendText,
23
23
  } from '../utils/legend-constants';
24
- import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y } from '../utils/title-constants';
24
+ import {
25
+ TITLE_FONT_SIZE,
26
+ TITLE_FONT_WEIGHT,
27
+ TITLE_Y,
28
+ } from '../utils/title-constants';
25
29
  import type { ParsedERDiagram, ERConstraint } from './types';
26
30
  import type { ERLayoutResult } from './layout';
27
31
  import { parseERDiagram } from './parser';
28
32
  import { layoutERDiagram } from './layout';
29
- import { classifyEREntities, ROLE_COLORS, ROLE_LABELS, ROLE_ORDER } from './classify';
33
+ import {
34
+ classifyEREntities,
35
+ ROLE_COLORS,
36
+ ROLE_LABELS,
37
+ ROLE_ORDER,
38
+ } from './classify';
30
39
  import type { EntityRole } from './classify';
31
40
 
32
41
  // ============================================================
@@ -50,10 +59,14 @@ const MEMBER_PADDING_X = 10;
50
59
 
51
60
  function constraintIcon(constraint: ERConstraint): string {
52
61
  switch (constraint) {
53
- case 'pk': return '\u2666'; // ♦
54
- case 'fk': return '\u2192'; //
55
- case 'unique': return '\u25C6'; // ◆
56
- case 'nullable': return '\u25CB'; //
62
+ case 'pk':
63
+ return '\u2666'; //
64
+ case 'fk':
65
+ return '\u2192'; //
66
+ case 'unique':
67
+ return '\u25C6'; // ◆
68
+ case 'nullable':
69
+ return '\u25CB'; // ○
57
70
  }
58
71
  }
59
72
 
@@ -61,7 +74,8 @@ function constraintIcon(constraint: ERConstraint): string {
61
74
  // Edge path generator
62
75
  // ============================================================
63
76
 
64
- const lineGenerator = d3Shape.line<{ x: number; y: number }>()
77
+ const lineGenerator = d3Shape
78
+ .line<{ x: number; y: number }>()
65
79
  .x((d) => d.x)
66
80
  .y((d) => d.y)
67
81
  .curve(d3Shape.curveBasis);
@@ -114,7 +128,8 @@ function drawCardinality(
114
128
  const offset = 15;
115
129
  const bx = point.x - ux * offset;
116
130
  const by = point.y - uy * offset;
117
- const labelText = cardinality === '1' ? '1' : cardinality === '*' ? '*' : '0..1';
131
+ const labelText =
132
+ cardinality === '1' ? '1' : cardinality === '*' ? '*' : '0..1';
118
133
  g.append('text')
119
134
  .attr('x', bx + px * 12)
120
135
  .attr('y', by + py * 12)
@@ -128,7 +143,7 @@ function drawCardinality(
128
143
 
129
144
  // Crow's foot notation
130
145
  const barOffset = 14; // how far back from the entity the bar sits
131
- const spread = 9; // half-width of the perpendicular bar / prong span
146
+ const spread = 9; // half-width of the perpendicular bar / prong span
132
147
 
133
148
  if (cardinality === '1') {
134
149
  // Single perpendicular bar
@@ -145,7 +160,7 @@ function drawCardinality(
145
160
  // Crow's foot: three prongs fan out FROM a point on the line
146
161
  // TOWARD the entity — the fork opens at the entity side.
147
162
  const forkOrigin = 18; // distance back from entity where prongs originate
148
- const forkEnd = 5; // distance from entity where prongs terminate
163
+ const forkEnd = 5; // distance from entity where prongs terminate
149
164
 
150
165
  // Origin point (on the line, away from entity)
151
166
  const ox = point.x - ux * forkOrigin;
@@ -241,7 +256,8 @@ export function renderERDiagram(
241
256
  // size at render time, which eliminates the stagger caused by reading clientWidth/
242
257
  // clientHeight before the container has stabilized.
243
258
  const naturalW = diagramW + DIAGRAM_PADDING * 2;
244
- const naturalH = diagramH + titleHeight + legendReserveH + DIAGRAM_PADDING * 2;
259
+ const naturalH =
260
+ diagramH + titleHeight + legendReserveH + DIAGRAM_PADDING * 2;
245
261
 
246
262
  // For export: scale the natural layout to fit the requested pixel dimensions.
247
263
  // For live preview: render at natural scale (scale=1) and let the SVG viewBox
@@ -292,7 +308,10 @@ export function renderERDiagram(
292
308
  .attr('fill', palette.text)
293
309
  .attr('font-size', TITLE_FONT_SIZE)
294
310
  .attr('font-weight', TITLE_FONT_WEIGHT)
295
- .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
311
+ .style(
312
+ 'cursor',
313
+ onClickItem && parsed.titleLineNumber ? 'pointer' : 'default'
314
+ )
296
315
  .text(parsed.title);
297
316
 
298
317
  if (parsed.titleLineNumber) {
@@ -300,8 +319,12 @@ export function renderERDiagram(
300
319
  if (onClickItem) {
301
320
  titleEl
302
321
  .on('click', () => onClickItem(parsed.titleLineNumber!))
303
- .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
304
- .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
322
+ .on('mouseenter', function () {
323
+ d3Selection.select(this).attr('opacity', 0.7);
324
+ })
325
+ .on('mouseleave', function () {
326
+ d3Selection.select(this).attr('opacity', 1);
327
+ });
305
328
  }
306
329
  }
307
330
  }
@@ -321,7 +344,8 @@ export function renderERDiagram(
321
344
  ? classifyEREntities(parsed.tables, parsed.relationships)
322
345
  : null;
323
346
  // semanticColorsActive defaults to true; false = legend collapsed, neutral color applied
324
- const semanticActive = semanticRoles !== null && (semanticColorsActive ?? true);
347
+ const semanticActive =
348
+ semanticRoles !== null && (semanticColorsActive ?? true);
325
349
 
326
350
  // ── Edges (behind nodes) ──
327
351
  const useLabels = parsed.options.notation === 'labels';
@@ -376,7 +400,8 @@ export function renderERDiagram(
376
400
  const bgW = labelLen * 7 + 8;
377
401
  const bgH = 16;
378
402
 
379
- edgeG.append('rect')
403
+ edgeG
404
+ .append('rect')
380
405
  .attr('x', midPt.x - bgW / 2)
381
406
  .attr('y', midPt.y - bgH / 2 - 1)
382
407
  .attr('width', bgW)
@@ -386,7 +411,8 @@ export function renderERDiagram(
386
411
  .attr('opacity', 0.85)
387
412
  .attr('class', 'er-edge-label-bg');
388
413
 
389
- edgeG.append('text')
414
+ edgeG
415
+ .append('text')
390
416
  .attr('x', midPt.x)
391
417
  .attr('y', midPt.y + 4)
392
418
  .attr('text-anchor', 'middle')
@@ -400,13 +426,23 @@ export function renderERDiagram(
400
426
  // ── Nodes (top layer) ──
401
427
  for (let ni = 0; ni < layout.nodes.length; ni++) {
402
428
  const node = layout.nodes[ni];
403
- const tagColor = resolveTagColor(node.metadata, parsed.tagGroups, activeTagGroup ?? null);
429
+ const tagColor = resolveTagColor(
430
+ node.metadata,
431
+ parsed.tagGroups,
432
+ activeTagGroup ?? null
433
+ );
404
434
  const semanticColor = semanticActive
405
- ? palette.colors[ROLE_COLORS[semanticRoles!.get(node.id) ?? 'unclassified']]
435
+ ? palette.colors[
436
+ ROLE_COLORS[semanticRoles!.get(node.id) ?? 'unclassified']
437
+ ]
406
438
  : semanticRoles
407
- ? palette.primary // neutral color when legend is collapsed
439
+ ? palette.primary // neutral color when legend is collapsed
408
440
  : undefined;
409
- const nodeColor = node.color ?? tagColor ?? semanticColor ?? seriesColors[ni % seriesColors.length];
441
+ const nodeColor =
442
+ node.color ??
443
+ tagColor ??
444
+ semanticColor ??
445
+ seriesColors[ni % seriesColors.length];
410
446
 
411
447
  const nodeG = contentG
412
448
  .append('g')
@@ -442,7 +478,8 @@ export function renderERDiagram(
442
478
  const stroke = nodeColor;
443
479
 
444
480
  // Outer rectangle
445
- nodeG.append('rect')
481
+ nodeG
482
+ .append('rect')
446
483
  .attr('x', -w / 2)
447
484
  .attr('y', -h / 2)
448
485
  .attr('width', w)
@@ -457,7 +494,8 @@ export function renderERDiagram(
457
494
  let yPos = -h / 2;
458
495
  const headerCenterY = yPos + node.headerHeight / 2;
459
496
 
460
- nodeG.append('text')
497
+ nodeG
498
+ .append('text')
461
499
  .attr('x', 0)
462
500
  .attr('y', headerCenterY)
463
501
  .attr('text-anchor', 'middle')
@@ -472,7 +510,8 @@ export function renderERDiagram(
472
510
  // Columns compartment
473
511
  if (node.columns.length > 0) {
474
512
  // Separator line
475
- nodeG.append('line')
513
+ nodeG
514
+ .append('line')
476
515
  .attr('x1', -w / 2)
477
516
  .attr('y1', yPos)
478
517
  .attr('x2', w / 2)
@@ -487,7 +526,8 @@ export function renderERDiagram(
487
526
  const iconX = -w / 2 + MEMBER_PADDING_X;
488
527
  const primaryConstraint = col.constraints[0];
489
528
  if (primaryConstraint) {
490
- nodeG.append('text')
529
+ nodeG
530
+ .append('text')
491
531
  .attr('x', iconX)
492
532
  .attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
493
533
  .attr('dominant-baseline', 'central')
@@ -501,7 +541,8 @@ export function renderERDiagram(
501
541
  let colText = col.name;
502
542
  if (col.type) colText += `: ${col.type}`;
503
543
 
504
- nodeG.append('text')
544
+ nodeG
545
+ .append('text')
505
546
  .attr('x', textX)
506
547
  .attr('y', memberY + MEMBER_LINE_HEIGHT / 2)
507
548
  .attr('dominant-baseline', 'central')
@@ -520,22 +561,23 @@ export function renderERDiagram(
520
561
  const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
521
562
  const LEGEND_GAP = 8;
522
563
 
523
- const legendG = svg.append('g')
524
- .attr('class', 'er-tag-legend');
564
+ const legendG = svg.append('g').attr('class', 'er-tag-legend');
525
565
 
526
566
  if (activeTagGroup) {
527
567
  legendG.attr('data-legend-active', activeTagGroup.toLowerCase());
528
568
  }
529
569
 
530
570
  let legendX = DIAGRAM_PADDING;
531
- let legendY = DIAGRAM_PADDING + titleHeight;
571
+ const legendY = DIAGRAM_PADDING + titleHeight;
532
572
 
533
573
  for (const group of parsed.tagGroups) {
534
- const groupG = legendG.append('g')
574
+ const groupG = legendG
575
+ .append('g')
535
576
  .attr('data-legend-group', group.name.toLowerCase());
536
577
 
537
578
  // Group label
538
- const labelText = groupG.append('text')
579
+ const labelText = groupG
580
+ .append('text')
539
581
  .attr('x', legendX)
540
582
  .attr('y', legendY + LEGEND_PILL_H / 2)
541
583
  .attr('dominant-baseline', 'central')
@@ -544,37 +586,47 @@ export function renderERDiagram(
544
586
  .attr('font-family', FONT_FAMILY)
545
587
  .text(`${group.name}:`);
546
588
 
547
- const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
589
+ const labelWidth =
590
+ (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) +
591
+ 6;
548
592
  legendX += labelWidth;
549
593
 
550
594
  // Entries
551
595
  for (const entry of group.entries) {
552
- const pillG = groupG.append('g')
596
+ const pillG = groupG
597
+ .append('g')
553
598
  .attr('data-legend-entry', entry.value.toLowerCase())
554
599
  .style('cursor', 'pointer');
555
600
 
556
601
  // Estimate text width
557
- const tmpText = legendG.append('text')
602
+ const tmpText = legendG
603
+ .append('text')
558
604
  .attr('font-size', LEGEND_PILL_FONT_SIZE)
559
605
  .attr('font-family', FONT_FAMILY)
560
606
  .text(entry.value);
561
- const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
607
+ const textW =
608
+ tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
562
609
  tmpText.remove();
563
610
 
564
611
  const pillW = textW + LEGEND_PILL_PAD * 2;
565
612
 
566
- pillG.append('rect')
613
+ pillG
614
+ .append('rect')
567
615
  .attr('x', legendX)
568
616
  .attr('y', legendY)
569
617
  .attr('width', pillW)
570
618
  .attr('height', LEGEND_PILL_H)
571
619
  .attr('rx', LEGEND_PILL_RX)
572
620
  .attr('ry', LEGEND_PILL_RX)
573
- .attr('fill', mix(entry.color, isDark ? palette.surface : palette.bg, 25))
621
+ .attr(
622
+ 'fill',
623
+ mix(entry.color, isDark ? palette.surface : palette.bg, 25)
624
+ )
574
625
  .attr('stroke', entry.color)
575
626
  .attr('stroke-width', 1);
576
627
 
577
- pillG.append('text')
628
+ pillG
629
+ .append('text')
578
630
  .attr('x', legendX + pillW / 2)
579
631
  .attr('y', legendY + LEGEND_PILL_H / 2)
580
632
  .attr('text-anchor', 'middle')
@@ -607,19 +659,25 @@ export function renderERDiagram(
607
659
  // Measure actual text widths for consistent spacing regardless of character mix.
608
660
  // Falls back to a character-count estimate in jsdom/test environments.
609
661
  const measureLabelW = (text: string, fontSize: number): number => {
610
- const dummy = svg.append('text')
662
+ const dummy = svg
663
+ .append('text')
611
664
  .attr('font-size', fontSize)
612
665
  .attr('font-family', FONT_FAMILY)
613
666
  .attr('visibility', 'hidden')
614
667
  .text(text);
615
- const measured = (dummy.node() as SVGTextElement | null)?.getComputedTextLength?.() ?? 0;
668
+ const measured =
669
+ (dummy.node() as SVGTextElement | null)?.getComputedTextLength?.() ??
670
+ 0;
616
671
  dummy.remove();
617
672
  return measured > 0 ? measured : text.length * fontSize * 0.6;
618
673
  };
619
674
 
620
675
  const labelWidths = new Map<EntityRole, number>();
621
676
  for (const role of presentRoles) {
622
- labelWidths.set(role, measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE));
677
+ labelWidths.set(
678
+ role,
679
+ measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
680
+ );
623
681
  }
624
682
 
625
683
  const groupBg = isDark
@@ -627,7 +685,8 @@ export function renderERDiagram(
627
685
  : mix(palette.surface, palette.bg, 30);
628
686
 
629
687
  const groupName = 'Role';
630
- const pillWidth = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
688
+ const pillWidth =
689
+ measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
631
690
  const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
632
691
 
633
692
  let totalWidth: number;
@@ -640,7 +699,11 @@ export function renderERDiagram(
640
699
  labelWidths.get(role)! +
641
700
  LEGEND_ENTRY_TRAIL;
642
701
  }
643
- totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + LEGEND_ENTRY_TRAIL + entriesWidth;
702
+ totalWidth =
703
+ LEGEND_CAPSULE_PAD * 2 +
704
+ pillWidth +
705
+ LEGEND_ENTRY_TRAIL +
706
+ entriesWidth;
644
707
  } else {
645
708
  totalWidth = pillWidth;
646
709
  }
@@ -764,7 +827,8 @@ export function renderERDiagramForExport(
764
827
 
765
828
  const container = document.createElement('div');
766
829
  const exportWidth = layout.width + DIAGRAM_PADDING * 2;
767
- const exportHeight = layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0);
830
+ const exportHeight =
831
+ layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0);
768
832
  container.style.width = `${exportWidth}px`;
769
833
  container.style.height = `${exportHeight}px`;
770
834
  container.style.position = 'absolute';
@@ -772,15 +836,10 @@ export function renderERDiagramForExport(
772
836
  document.body.appendChild(container);
773
837
 
774
838
  try {
775
- renderERDiagram(
776
- container,
777
- parsed,
778
- layout,
779
- palette,
780
- isDark,
781
- undefined,
782
- { width: exportWidth, height: exportHeight }
783
- );
839
+ renderERDiagram(container, parsed, layout, palette, isDark, undefined, {
840
+ width: exportWidth,
841
+ height: exportHeight,
842
+ });
784
843
 
785
844
  const svgEl = container.querySelector('svg');
786
845
  if (!svgEl) return '';
@@ -18,7 +18,6 @@ import type {
18
18
  GanttGroup,
19
19
  GanttHolidays,
20
20
  ResolvedSchedule,
21
- ResolvedTask,
22
21
  ResolvedGroup,
23
22
  Offset,
24
23
  } from './types';
@@ -27,7 +26,6 @@ import {
27
26
  addGanttDuration,
28
27
  buildHolidaySet,
29
28
  parseGanttDate,
30
- daysBetween,
31
29
  } from '../utils/duration';
32
30
 
33
31
  // ── Internal types ──────────────────────────────────────────
@@ -65,7 +63,8 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
65
63
  diagnostics.push(makeDgmoError(line, message, 'warning'));
66
64
  };
67
65
 
68
- const fail = (line: number, message: string): ResolvedSchedule => {
66
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
67
+ const _fail = (line: number, message: string): ResolvedSchedule => {
69
68
  const diag = makeDgmoError(line, message);
70
69
  diagnostics.push(diag);
71
70
  result.error = formatDgmoError(diag);
@@ -115,7 +114,8 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
115
114
  // ── Resolve explicit -> dependencies ────────────────────
116
115
 
117
116
  for (const task of allTasks) {
118
- const node = taskMap.get(task.id)!;
117
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
118
+ const _node = taskMap.get(task.id)!;
119
119
  for (const dep of task.dependencies) {
120
120
  const resolved = resolveTaskName(dep.targetName, allTasks);
121
121
  if (isResolverError(resolved)) {
@@ -129,7 +129,10 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
129
129
  if (targetNode) {
130
130
  // Check for redundant dependency (already a sequential predecessor)
131
131
  if (targetNode.predecessors.includes(task.id)) {
132
- warn(dep.lineNumber, `Redundant dependency: "${dep.targetName}" already follows "${task.label}" sequentially. Did you mean to wrap groups in \`parallel\`?`);
132
+ warn(
133
+ dep.lineNumber,
134
+ `Redundant dependency: "${dep.targetName}" already follows "${task.label}" sequentially. Did you mean to wrap groups in \`parallel\`?`
135
+ );
133
136
  } else {
134
137
  targetNode.predecessors.push(task.id);
135
138
  // Store dep offset info — we need it during scheduling
@@ -147,10 +150,10 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
147
150
  if (!sortedIds) {
148
151
  // Find cycle, warn, and break it by removing one explicit dep edge
149
152
  const cycle = findCycle(taskMap);
150
- const cycleStr = cycle.map(id => taskMap.get(id)!.task.label).join(' → ');
153
+ const cycleStr = cycle.map((id) => taskMap.get(id)!.task.label).join(' → ');
151
154
  warn(
152
155
  taskMap.get(cycle[0])!.task.lineNumber,
153
- `Circular dependency detected: ${cycleStr}. The cycle-creating dependency was dropped.`,
156
+ `Circular dependency detected: ${cycleStr}. The cycle-creating dependency was dropped.`
154
157
  );
155
158
 
156
159
  // Remove the last edge in the cycle to break it
@@ -200,7 +203,13 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
200
203
  // Apply dep offset if present
201
204
  const depOffset = depOffsetMap.get(`${predId}->${taskId}`);
202
205
  if (depOffset) {
203
- predEnd = addGanttDuration(predEnd, depOffset.duration, parsed.holidays, holidaySet, depOffset.direction);
206
+ predEnd = addGanttDuration(
207
+ predEnd,
208
+ depOffset.duration,
209
+ parsed.holidays,
210
+ holidaySet,
211
+ depOffset.direction
212
+ );
204
213
  }
205
214
 
206
215
  if (predEnd.getTime() > start.getTime()) {
@@ -211,13 +220,25 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
211
220
 
212
221
  // Apply task-level offset (shifts start forward or backward)
213
222
  if (task.offset) {
214
- start = addGanttDuration(start, task.offset.duration, parsed.holidays, holidaySet, task.offset.direction);
223
+ start = addGanttDuration(
224
+ start,
225
+ task.offset.duration,
226
+ parsed.holidays,
227
+ holidaySet,
228
+ task.offset.direction
229
+ );
215
230
  if (start.getTime() < projectStart.getTime()) {
216
- warn(task.lineNumber, `Negative offset on task '${task.label}' exceeds available range; start clamped to project start.`);
231
+ warn(
232
+ task.lineNumber,
233
+ `Negative offset on task '${task.label}' exceeds available range; start clamped to project start.`
234
+ );
217
235
  start = new Date(projectStart);
218
236
  }
219
237
  } else if (start.getTime() < projectStart.getTime()) {
220
- warn(task.lineNumber, `Negative offset on dependency exceeds available range; start of '${task.label}' clamped to project start.`);
238
+ warn(
239
+ task.lineNumber,
240
+ `Negative offset on dependency exceeds available range; start of '${task.label}' clamped to project start.`
241
+ );
221
242
  start = new Date(projectStart);
222
243
  }
223
244
 
@@ -226,12 +247,18 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
226
247
  let maxPredEnd = new Date(0);
227
248
  for (const predId of node.predecessors) {
228
249
  const predNode = taskMap.get(predId)!;
229
- if (predNode.endDate && predNode.endDate.getTime() > maxPredEnd.getTime()) {
250
+ if (
251
+ predNode.endDate &&
252
+ predNode.endDate.getTime() > maxPredEnd.getTime()
253
+ ) {
230
254
  maxPredEnd = predNode.endDate;
231
255
  }
232
256
  }
233
257
  if (start.getTime() < maxPredEnd.getTime()) {
234
- warn(task.lineNumber, `Explicit date ${task.explicitStart}${task.offset ? ' (with offset)' : ''} overlaps with predecessor ending ${formatDate(maxPredEnd)}. Using explicit date.`);
258
+ warn(
259
+ task.lineNumber,
260
+ `Explicit date ${task.explicitStart}${task.offset ? ' (with offset)' : ''} overlaps with predecessor ending ${formatDate(maxPredEnd)}. Using explicit date.`
261
+ );
235
262
  }
236
263
  }
237
264
 
@@ -242,7 +269,12 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
242
269
  // Milestone: zero duration, end = start
243
270
  end = new Date(start);
244
271
  } else {
245
- end = addGanttDuration(start, task.duration, parsed.holidays, holidaySet);
272
+ end = addGanttDuration(
273
+ start,
274
+ task.duration,
275
+ parsed.holidays,
276
+ holidaySet
277
+ );
246
278
  }
247
279
  } else {
248
280
  // Explicit date task with no duration = milestone at that date
@@ -257,7 +289,13 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
257
289
 
258
290
  // Critical path calculation (if enabled)
259
291
  const criticalSet = parsed.options.criticalPath
260
- ? computeCriticalPath(sortedIds, taskMap, depOffsetMap, parsed.holidays, holidaySet)
292
+ ? computeCriticalPath(
293
+ sortedIds,
294
+ taskMap,
295
+ depOffsetMap,
296
+ parsed.holidays,
297
+ holidaySet
298
+ )
261
299
  : new Set<string>();
262
300
 
263
301
  // Cascading uncertainty: uncertain if task itself is uncertain OR any predecessor is
@@ -284,7 +322,9 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
284
322
  endDate: node.endDate!,
285
323
  isCriticalPath: criticalSet.has(taskId),
286
324
  isUncertain: uncertainSet.has(taskId),
287
- isMilestone: (node.task.duration?.amount === 0) || (!node.task.duration && !node.task.explicitStart),
325
+ isMilestone:
326
+ node.task.duration?.amount === 0 ||
327
+ (!node.task.duration && !node.task.explicitStart),
288
328
  groupPath: node.task.groupPath,
289
329
  effectiveMetadata: node.task.metadata,
290
330
  });
@@ -310,12 +350,14 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
310
350
  // ── Warnings ────────────────────────────────────────────
311
351
 
312
352
  // Missing parallel warning: 2+ top-level groups without parallel wrapper
313
- const topLevelGroups = parsed.nodes.filter(n => n.kind === 'group');
353
+ const topLevelGroups = parsed.nodes.filter((n) => n.kind === 'group');
314
354
  if (topLevelGroups.length >= 2) {
315
- const names = topLevelGroups.map(g => (g as GanttGroup & { kind: 'group' }).name);
355
+ const names = topLevelGroups.map(
356
+ (g) => (g as GanttGroup & { kind: 'group' }).name
357
+ );
316
358
  warn(
317
359
  topLevelGroups[0].lineNumber,
318
- `${names.join(' and ')} are sequential. Wrap in \`parallel\` if they should run concurrently.`,
360
+ `${names.join(' and ')} are sequential. Wrap in \`parallel\` if they should run concurrently.`
319
361
  );
320
362
  }
321
363
 
@@ -332,7 +374,7 @@ export function calculateSchedule(parsed: ParsedGantt): ResolvedSchedule {
332
374
  */
333
375
  function buildImplicitDeps(
334
376
  nodes: GanttNode[],
335
- taskMap: Map<string, TaskNode>,
377
+ taskMap: Map<string, TaskNode>
336
378
  ): void {
337
379
  walkChildren(nodes, null);
338
380
 
@@ -408,7 +450,10 @@ function buildImplicitDeps(
408
450
  }
409
451
  }
410
452
 
411
- function walkSequential(children: GanttNode[], afterTaskId: string | null): string | null {
453
+ function walkSequential(
454
+ children: GanttNode[],
455
+ afterTaskId: string | null
456
+ ): string | null {
412
457
  let prevTaskId = afterTaskId;
413
458
  for (const node of children) {
414
459
  if (node.kind === 'task') {
@@ -545,7 +590,7 @@ function findCycle(taskMap: Map<string, TaskNode>): string[] {
545
590
  function breakCycle(
546
591
  cycle: string[],
547
592
  taskMap: Map<string, TaskNode>,
548
- depOffsetMap: Map<string, Offset>,
593
+ depOffsetMap: Map<string, Offset>
549
594
  ): void {
550
595
  if (cycle.length < 3) return; // need at least [A, B, A]
551
596
  // Remove the edge from second-to-last → first (i.e. the edge that closes the cycle)
@@ -568,7 +613,7 @@ function computeCriticalPath(
568
613
  taskMap: Map<string, TaskNode>,
569
614
  depOffsetMap: Map<string, Offset>,
570
615
  holidays: GanttHolidays,
571
- holidaySet: Set<string>,
616
+ holidaySet: Set<string>
572
617
  ): Set<string> {
573
618
  if (sortedIds.length === 0) return new Set();
574
619
 
@@ -610,7 +655,13 @@ function computeCriticalPath(
610
655
  const succTask = taskMap.get(succId)!.task;
611
656
  if (succTask.offset) {
612
657
  const reverseDir = (succTask.offset.direction * -1) as 1 | -1;
613
- const adjusted = addGanttDuration(new Date(succLS), succTask.offset.duration, holidays, holidaySet, reverseDir);
658
+ const adjusted = addGanttDuration(
659
+ new Date(succLS),
660
+ succTask.offset.duration,
661
+ holidays,
662
+ holidaySet,
663
+ reverseDir
664
+ );
614
665
  succLS = adjusted.getTime();
615
666
  }
616
667
 
@@ -618,7 +669,13 @@ function computeCriticalPath(
618
669
  const depOffset = depOffsetMap.get(`${id}->${succId}`);
619
670
  if (depOffset) {
620
671
  const reverseDir = (depOffset.direction * -1) as 1 | -1;
621
- const adjusted = addGanttDuration(new Date(succLS), depOffset.duration, holidays, holidaySet, reverseDir);
672
+ const adjusted = addGanttDuration(
673
+ new Date(succLS),
674
+ depOffset.duration,
675
+ holidays,
676
+ holidaySet,
677
+ reverseDir
678
+ );
622
679
  succLS = adjusted.getTime();
623
680
  }
624
681
 
@@ -651,7 +708,7 @@ function buildResolvedGroups(
651
708
  nodes: GanttNode[],
652
709
  taskMap: Map<string, TaskNode>,
653
710
  groups: ResolvedGroup[],
654
- depth: number,
711
+ depth: number
655
712
  ): void {
656
713
  for (const node of nodes) {
657
714
  if (node.kind === 'group') {
@@ -679,8 +736,10 @@ function buildResolvedGroups(
679
736
  for (const task of childTasks) {
680
737
  const resolved = taskMap.get(task.id);
681
738
  if (!resolved?.startDate || !resolved?.endDate) continue;
682
- if (resolved.startDate.getTime() < minStart) minStart = resolved.startDate.getTime();
683
- if (resolved.endDate.getTime() > maxEnd) maxEnd = resolved.endDate.getTime();
739
+ if (resolved.startDate.getTime() < minStart)
740
+ minStart = resolved.startDate.getTime();
741
+ if (resolved.endDate.getTime() > maxEnd)
742
+ maxEnd = resolved.endDate.getTime();
684
743
  const dur = resolved.endDate.getTime() - resolved.startDate.getTime();
685
744
  totalDuration += dur;
686
745
  if (task.progress !== null) {
@@ -695,7 +754,10 @@ function buildResolvedGroups(
695
754
  metadata: node.metadata,
696
755
  startDate: new Date(minStart === Infinity ? 0 : minStart),
697
756
  endDate: new Date(maxEnd === -Infinity ? 0 : maxEnd),
698
- progress: hasProgress && totalDuration > 0 ? totalProgress / totalDuration : null,
757
+ progress:
758
+ hasProgress && totalDuration > 0
759
+ ? totalProgress / totalDuration
760
+ : null,
699
761
  lineNumber: node.lineNumber,
700
762
  depth,
701
763
  });