@diagrammo/dgmo 0.8.2 → 0.8.4

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 (120) 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 +185 -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 +189 -194
  9. package/dist/editor.cjs +336 -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 +305 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/index.cjs +3699 -1564
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +7 -6
  18. package/dist/index.d.ts +7 -6
  19. package/dist/index.js +3699 -1564
  20. package/dist/index.js.map +1 -1
  21. package/docs/language-reference.md +822 -1060
  22. package/gallery/fixtures/arc.dgmo +18 -0
  23. package/gallery/fixtures/area.dgmo +19 -0
  24. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  25. package/gallery/fixtures/bar.dgmo +10 -0
  26. package/gallery/fixtures/c4-full.dgmo +52 -0
  27. package/gallery/fixtures/c4.dgmo +17 -0
  28. package/gallery/fixtures/chord.dgmo +12 -0
  29. package/gallery/fixtures/class-basic.dgmo +14 -0
  30. package/gallery/fixtures/class-full.dgmo +43 -0
  31. package/gallery/fixtures/doughnut.dgmo +8 -0
  32. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  33. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  34. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  35. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  36. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  37. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  38. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  39. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  40. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  41. package/gallery/fixtures/function.dgmo +8 -0
  42. package/gallery/fixtures/funnel.dgmo +7 -0
  43. package/gallery/fixtures/gantt-full.dgmo +49 -0
  44. package/gallery/fixtures/gantt.dgmo +42 -0
  45. package/gallery/fixtures/heatmap.dgmo +8 -0
  46. package/gallery/fixtures/infra-full.dgmo +78 -0
  47. package/gallery/fixtures/infra-overload.dgmo +25 -0
  48. package/gallery/fixtures/infra.dgmo +47 -0
  49. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  50. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  51. package/gallery/fixtures/initiative-status.dgmo +9 -0
  52. package/gallery/fixtures/line.dgmo +19 -0
  53. package/gallery/fixtures/multi-line.dgmo +11 -0
  54. package/gallery/fixtures/org-basic.dgmo +16 -0
  55. package/gallery/fixtures/org-full.dgmo +69 -0
  56. package/gallery/fixtures/org-teams.dgmo +25 -0
  57. package/gallery/fixtures/pie.dgmo +9 -0
  58. package/gallery/fixtures/polar-area.dgmo +8 -0
  59. package/gallery/fixtures/quadrant.dgmo +18 -0
  60. package/gallery/fixtures/radar.dgmo +8 -0
  61. package/gallery/fixtures/sankey.dgmo +31 -0
  62. package/gallery/fixtures/scatter.dgmo +21 -0
  63. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  64. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  65. package/gallery/fixtures/sequence.dgmo +35 -0
  66. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  67. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  68. package/gallery/fixtures/slope.dgmo +8 -0
  69. package/gallery/fixtures/spr-eras.dgmo +62 -0
  70. package/gallery/fixtures/state.dgmo +30 -0
  71. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  72. package/gallery/fixtures/timeline.dgmo +32 -0
  73. package/gallery/fixtures/venn.dgmo +10 -0
  74. package/gallery/fixtures/wordcloud.dgmo +24 -0
  75. package/package.json +51 -2
  76. package/src/c4/layout.ts +372 -90
  77. package/src/c4/parser.ts +113 -62
  78. package/src/chart.ts +149 -64
  79. package/src/class/parser.ts +84 -28
  80. package/src/class/renderer.ts +2 -2
  81. package/src/cli.ts +179 -77
  82. package/src/completion.ts +381 -182
  83. package/src/d3.ts +1026 -428
  84. package/src/dgmo-mermaid.ts +16 -13
  85. package/src/dgmo-router.ts +70 -24
  86. package/src/echarts.ts +682 -169
  87. package/src/editor/dgmo.grammar +69 -0
  88. package/src/editor/dgmo.grammar.d.ts +2 -0
  89. package/src/editor/dgmo.grammar.js +18 -0
  90. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  91. package/src/editor/dgmo.grammar.terms.js +35 -0
  92. package/src/editor/highlight.ts +36 -0
  93. package/src/editor/index.ts +28 -0
  94. package/src/editor/keywords.ts +220 -0
  95. package/src/editor/tokens.ts +30 -0
  96. package/src/er/parser.ts +55 -29
  97. package/src/er/renderer.ts +112 -53
  98. package/src/gantt/calculator.ts +91 -29
  99. package/src/gantt/parser.ts +291 -97
  100. package/src/gantt/renderer.ts +1120 -350
  101. package/src/graph/flowchart-parser.ts +48 -75
  102. package/src/graph/state-parser.ts +54 -27
  103. package/src/infra/parser.ts +161 -177
  104. package/src/infra/renderer.ts +723 -271
  105. package/src/infra/types.ts +0 -1
  106. package/src/initiative-status/parser.ts +144 -56
  107. package/src/kanban/parser.ts +27 -19
  108. package/src/org/layout.ts +111 -44
  109. package/src/org/parser.ts +71 -27
  110. package/src/org/resolver.ts +3 -3
  111. package/src/palettes/index.ts +3 -2
  112. package/src/render.ts +1 -2
  113. package/src/sequence/parser.ts +209 -100
  114. package/src/sitemap/parser.ts +73 -44
  115. package/src/utils/arrows.ts +2 -22
  116. package/src/utils/duration.ts +39 -21
  117. package/src/utils/legend-constants.ts +0 -2
  118. package/src/utils/parsing.ts +82 -72
  119. package/src/utils/tag-groups.ts +4 -41
  120. package/src/infra/serialize.ts +0 -67
package/src/org/layout.ts CHANGED
@@ -6,7 +6,11 @@ import { hierarchy, tree } from 'd3-hierarchy';
6
6
  import type { ParsedOrg, OrgNode } from './parser';
7
7
  import type { TagGroup } from '../utils/tag-groups';
8
8
  import { resolveTagColor, injectDefaultTagMetadata } from '../utils/tag-groups';
9
- import { LEGEND_PILL_FONT_SIZE, LEGEND_ENTRY_FONT_SIZE, measureLegendText } from '../utils/legend-constants';
9
+ import {
10
+ LEGEND_PILL_FONT_SIZE,
11
+ LEGEND_ENTRY_FONT_SIZE,
12
+ measureLegendText,
13
+ } from '../utils/legend-constants';
10
14
 
11
15
  // ============================================================
12
16
  // Types
@@ -98,9 +102,7 @@ const CONTAINER_LABEL_HEIGHT = 28;
98
102
  const CONTAINER_META_LINE_HEIGHT = 16;
99
103
  const STACK_V_GAP = 20;
100
104
 
101
-
102
105
  // Legend (kanban-style pills)
103
- const LEGEND_GAP = 30;
104
106
  const LEGEND_HEIGHT = 28;
105
107
  const LEGEND_PILL_PAD = 16;
106
108
  const LEGEND_CAPSULE_PAD = 4;
@@ -116,10 +118,14 @@ const LEGEND_EYE_GAP = 6;
116
118
  // ============================================================
117
119
 
118
120
  /** Count all non-container descendants recursively, including hidden (collapsed) ones. */
119
- function countDescendantNodes(node: OrgNode, hiddenCounts?: Map<string, number>): number {
121
+ function countDescendantNodes(
122
+ node: OrgNode,
123
+ hiddenCounts?: Map<string, number>
124
+ ): number {
120
125
  let count = 0;
121
126
  for (const child of node.children) {
122
- count += (child.isContainer ? 0 : 1) + countDescendantNodes(child, hiddenCounts);
127
+ count +=
128
+ (child.isContainer ? 0 : 1) + countDescendantNodes(child, hiddenCounts);
123
129
  const hc = hiddenCounts?.get(child.id);
124
130
  if (hc) count += hc;
125
131
  }
@@ -152,13 +158,18 @@ function computeCardWidth(label: string, meta: Record<string, string>): number {
152
158
  if (lineChars > maxChars) maxChars = lineChars;
153
159
  }
154
160
 
155
- return Math.max(MIN_CARD_WIDTH, Math.ceil(maxChars * CHAR_WIDTH) + CARD_H_PAD * 2);
161
+ return Math.max(
162
+ MIN_CARD_WIDTH,
163
+ Math.ceil(maxChars * CHAR_WIDTH) + CARD_H_PAD * 2
164
+ );
156
165
  }
157
166
 
158
167
  function computeCardHeight(meta: Record<string, string>): number {
159
168
  const metaCount = Object.keys(meta).length;
160
169
  if (metaCount === 0) return HEADER_HEIGHT + CARD_V_PAD;
161
- return HEADER_HEIGHT + SEPARATOR_GAP + metaCount * META_LINE_HEIGHT + CARD_V_PAD;
170
+ return (
171
+ HEADER_HEIGHT + SEPARATOR_GAP + metaCount * META_LINE_HEIGHT + CARD_V_PAD
172
+ );
162
173
  }
163
174
 
164
175
  // ============================================================
@@ -172,7 +183,12 @@ function resolveNodeColor(
172
183
  ): string | undefined {
173
184
  // Explicit inline (color) always wins — handled before tag resolution
174
185
  if (node.color) return node.color;
175
- return resolveTagColor(node.metadata, tagGroups, activeGroupName, node.isContainer);
186
+ return resolveTagColor(
187
+ node.metadata,
188
+ tagGroups,
189
+ activeGroupName,
190
+ node.isContainer
191
+ );
176
192
  }
177
193
 
178
194
  // ============================================================
@@ -204,7 +220,13 @@ function buildTreeNodes(
204
220
  }
205
221
  return {
206
222
  orgNode,
207
- children: buildTreeNodes(orgNode.children, hiddenCounts, hiddenAttributes, subNodeLabel, showSubNodeCount),
223
+ children: buildTreeNodes(
224
+ orgNode.children,
225
+ hiddenCounts,
226
+ hiddenAttributes,
227
+ subNodeLabel,
228
+ showSubNodeCount
229
+ ),
208
230
  width: computeCardWidth(orgNode.label, meta),
209
231
  height: computeCardHeight(meta),
210
232
  };
@@ -279,7 +301,8 @@ function computeLegendGroups(
279
301
  if (visibleEntries.length === 0) continue;
280
302
 
281
303
  // Pill label shows just the group name (alias is for DSL shorthand only)
282
- const pillWidth = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
304
+ const pillWidth =
305
+ measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
283
306
  const minPillWidth = pillWidth;
284
307
 
285
308
  // Capsule: pad + pill + gap + entries + pad
@@ -318,10 +341,7 @@ function computeLegendGroups(
318
341
  * Inject default tag group values into non-container node metadata.
319
342
  * Delegates to shared `injectDefaultTagMetadata` with org-specific skip logic.
320
343
  */
321
- function injectDefaultMetadata(
322
- roots: OrgNode[],
323
- tagGroups: TagGroup[]
324
- ): void {
344
+ function injectDefaultMetadata(roots: OrgNode[], tagGroups: TagGroup[]): void {
325
345
  // Flatten all nodes (recursive) for the shared utility
326
346
  const allNodes: OrgNode[] = [];
327
347
  const collect = (node: OrgNode) => {
@@ -349,7 +369,14 @@ export function layoutOrg(
349
369
  const showEyeIcons = hiddenAttributes !== undefined;
350
370
  const legendGroups = computeLegendGroups(parsed.tagGroups, showEyeIcons);
351
371
  if (legendGroups.length === 0) {
352
- return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
372
+ return {
373
+ nodes: [],
374
+ edges: [],
375
+ containers: [],
376
+ legend: [],
377
+ width: 0,
378
+ height: 0,
379
+ };
353
380
  }
354
381
 
355
382
  // Legend-only mode: stack groups vertically, all expanded
@@ -377,8 +404,16 @@ export function layoutOrg(
377
404
 
378
405
  // Build tree structure
379
406
  const subNodeLabel = parsed.options['sub-node-label'] ?? undefined;
380
- const showSubNodeCount = ['yes', 'on'].includes(parsed.options['show-sub-node-count']?.toLowerCase() ?? '');
381
- const treeNodes = buildTreeNodes(parsed.roots, hiddenCounts, hiddenAttributes, subNodeLabel, showSubNodeCount);
407
+ const showSubNodeCount = ['yes', 'on'].includes(
408
+ parsed.options['show-sub-node-count']?.toLowerCase() ?? ''
409
+ );
410
+ const treeNodes = buildTreeNodes(
411
+ parsed.roots,
412
+ hiddenCounts,
413
+ hiddenAttributes,
414
+ subNodeLabel,
415
+ showSubNodeCount
416
+ );
382
417
 
383
418
  // Single root or virtual root for multiple roots
384
419
  let root: TreeNode;
@@ -492,9 +527,9 @@ export function layoutOrg(
492
527
  // Y positions so each level's gap is based on the actual max height at that
493
528
  // level rather than the global max.
494
529
  {
495
- const descendants = h.descendants().filter(
496
- (d) => d.data.orgNode.id !== '__virtual_root__'
497
- );
530
+ const descendants = h
531
+ .descendants()
532
+ .filter((d) => d.data.orgNode.id !== '__virtual_root__');
498
533
 
499
534
  // Collect max actual card height per depth level.
500
535
  // Exclude __stack_ placeholders — their aggregate height (multiple
@@ -597,7 +632,7 @@ export function layoutOrg(
597
632
  // D3 uses uniform nodeSize so narrow stacks get the same gap as wide
598
633
  // subtrees. Process bottom-up so inner subtrees are compact first.
599
634
  {
600
- type HNode = (typeof h);
635
+ type HNode = typeof h;
601
636
  const subtreeExtent = (node: HNode): { minX: number; maxX: number } => {
602
637
  // Start with this node's own card/header bounds
603
638
  let min = node.x! - node.data.width / 2;
@@ -652,8 +687,7 @@ export function layoutOrg(
652
687
  positions[i] = prevRight + H_GAP - extents[i].relLeft;
653
688
  }
654
689
 
655
- const newCenter =
656
- (positions[0] + positions[positions.length - 1]) / 2;
690
+ const newCenter = (positions[0] + positions[positions.length - 1]) / 2;
657
691
  const centerShift = currentCenter - newCenter;
658
692
 
659
693
  for (let i = 0; i < children.length; i++) {
@@ -761,7 +795,11 @@ export function layoutOrg(
761
795
  for (const ec of expandedChildren) {
762
796
  const hc = hiddenCounts?.get(ec.orgNode.id);
763
797
  const meta = filterMetadata(ec.orgNode.metadata, hiddenAttributes);
764
- if (!ec.orgNode.isContainer && showSubNodeCount && !(hc != null && hc > 0)) {
798
+ if (
799
+ !ec.orgNode.isContainer &&
800
+ showSubNodeCount &&
801
+ !(hc != null && hc > 0)
802
+ ) {
765
803
  const count = countDescendantNodes(ec.orgNode, hiddenCounts);
766
804
  if (count > 0) meta[subNodeKey] = String(count);
767
805
  }
@@ -771,13 +809,18 @@ export function layoutOrg(
771
809
  metadata: meta,
772
810
  isContainer: ec.orgNode.isContainer,
773
811
  lineNumber: ec.orgNode.lineNumber,
774
- color: resolveNodeColor(ec.orgNode, parsed.tagGroups, activeTagGroup ?? null),
812
+ color: resolveNodeColor(
813
+ ec.orgNode,
814
+ parsed.tagGroups,
815
+ activeTagGroup ?? null
816
+ ),
775
817
  x: ec.cx + offsetX,
776
818
  y: ec.cy + offsetY,
777
819
  width: ec.width,
778
820
  height: ec.height,
779
821
  hiddenCount: hc,
780
- hasChildren: (ec.orgNode.children.length > 0 || (hc != null && hc > 0)) || undefined,
822
+ hasChildren:
823
+ ec.orgNode.children.length > 0 || (hc != null && hc > 0) || undefined,
781
824
  });
782
825
  }
783
826
 
@@ -813,13 +856,20 @@ export function layoutOrg(
813
856
  metadata: nodeMeta,
814
857
  isContainer: orgNode.isContainer,
815
858
  lineNumber: orgNode.lineNumber,
816
- color: resolveNodeColor(orgNode, parsed.tagGroups, activeTagGroup ?? null),
859
+ color: resolveNodeColor(
860
+ orgNode,
861
+ parsed.tagGroups,
862
+ activeTagGroup ?? null
863
+ ),
817
864
  x,
818
865
  y,
819
866
  width: w,
820
867
  height: ht,
821
868
  hiddenCount: hc,
822
- hasChildren: (d.children != null && d.children.length > 0) || (hc != null && hc > 0) || undefined,
869
+ hasChildren:
870
+ (d.children != null && d.children.length > 0) ||
871
+ (hc != null && hc > 0) ||
872
+ undefined,
823
873
  });
824
874
 
825
875
  // Collect children per parent for bus-style edge generation
@@ -912,11 +962,12 @@ export function layoutOrg(
912
962
 
913
963
  // Compute container bounds from d3 hierarchy (bottom-up so inner
914
964
  // container boxes are available when computing outer containers)
915
- const allContainerNodes = h.descendants().filter(
916
- (d) =>
917
- d.data.orgNode.id !== '__virtual_root__' &&
918
- d.data.orgNode.isContainer
919
- );
965
+ const allContainerNodes = h
966
+ .descendants()
967
+ .filter(
968
+ (d) =>
969
+ d.data.orgNode.id !== '__virtual_root__' && d.data.orgNode.isContainer
970
+ );
920
971
 
921
972
  // Map from node ID to computed visual bounds (offset-space)
922
973
  const containerBoundsMap = new Map<
@@ -938,7 +989,10 @@ export function layoutOrg(
938
989
  const labelHeight =
939
990
  CONTAINER_LABEL_HEIGHT + metaCount * CONTAINER_META_LINE_HEIGHT;
940
991
  const boxWidth = d.data.width;
941
- const boxHeight = Math.max(labelHeight + CONTAINER_PAD_BOTTOM, EMPTY_CONTAINER_MIN_HEIGHT);
992
+ const boxHeight = Math.max(
993
+ labelHeight + CONTAINER_PAD_BOTTOM,
994
+ EMPTY_CONTAINER_MIN_HEIGHT
995
+ );
942
996
  const boxX = cx - boxWidth / 2;
943
997
  const boxY = cy;
944
998
 
@@ -955,7 +1009,11 @@ export function layoutOrg(
955
1009
  nodeId: d.data.orgNode.id,
956
1010
  label: d.data.orgNode.label,
957
1011
  lineNumber: d.data.orgNode.lineNumber,
958
- color: resolveNodeColor(d.data.orgNode, parsed.tagGroups, activeTagGroup ?? null),
1012
+ color: resolveNodeColor(
1013
+ d.data.orgNode,
1014
+ parsed.tagGroups,
1015
+ activeTagGroup ?? null
1016
+ ),
959
1017
  metadata: cMeta,
960
1018
  x: boxX,
961
1019
  y: boxY,
@@ -975,7 +1033,7 @@ export function layoutOrg(
975
1033
 
976
1034
  for (const d of containerCandidates) {
977
1035
  // Collect all descendants (not just direct children)
978
- const allDesc: typeof d[] = [];
1036
+ const allDesc: (typeof d)[] = [];
979
1037
  const collectDesc = (node: typeof d) => {
980
1038
  if (node.children) {
981
1039
  for (const child of node.children) {
@@ -1044,9 +1102,7 @@ export function layoutOrg(
1044
1102
  const finalBoxWidth = Math.max(contentWidth, d.data.width);
1045
1103
  // Center the box if the label is wider than the content
1046
1104
  const centeredBoxX =
1047
- finalBoxWidth > contentWidth
1048
- ? containerX - finalBoxWidth / 2
1049
- : boxX;
1105
+ finalBoxWidth > contentWidth ? containerX - finalBoxWidth / 2 : boxX;
1050
1106
 
1051
1107
  // Store bounds for parent containers to reference
1052
1108
  containerBoundsMap.set(d.data.orgNode.id, {
@@ -1062,7 +1118,11 @@ export function layoutOrg(
1062
1118
  nodeId: d.data.orgNode.id,
1063
1119
  label: d.data.orgNode.label,
1064
1120
  lineNumber: d.data.orgNode.lineNumber,
1065
- color: resolveNodeColor(d.data.orgNode, parsed.tagGroups, activeTagGroup ?? null),
1121
+ color: resolveNodeColor(
1122
+ d.data.orgNode,
1123
+ parsed.tagGroups,
1124
+ activeTagGroup ?? null
1125
+ ),
1066
1126
  metadata: cMeta2,
1067
1127
  x: centeredBoxX,
1068
1128
  y: boxY,
@@ -1111,16 +1171,23 @@ export function layoutOrg(
1111
1171
 
1112
1172
  // Compute legend for tag groups
1113
1173
  const showEyeIcons = hiddenAttributes !== undefined;
1114
- const legendGroups = computeLegendGroups(parsed.tagGroups, showEyeIcons, usedValuesByGroup);
1174
+ const legendGroups = computeLegendGroups(
1175
+ parsed.tagGroups,
1176
+ showEyeIcons,
1177
+ usedValuesByGroup
1178
+ );
1115
1179
  let finalWidth = totalWidth;
1116
1180
  let finalHeight = totalHeight;
1117
1181
 
1118
1182
  // When a tag group is active, only that group is laid out (full size).
1119
1183
  // When none is active, all groups are laid out minified — unless
1120
1184
  // expandAllLegend is set (export mode), which shows all groups expanded.
1121
- const visibleGroups = activeTagGroup != null
1122
- ? legendGroups.filter((g) => g.name.toLowerCase() === activeTagGroup.toLowerCase())
1123
- : legendGroups;
1185
+ const visibleGroups =
1186
+ activeTagGroup != null
1187
+ ? legendGroups.filter(
1188
+ (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
1189
+ )
1190
+ : legendGroups;
1124
1191
  const allExpanded = expandAllLegend && activeTagGroup == null;
1125
1192
  const effectiveW = (g: OrgLegendGroup) =>
1126
1193
  activeTagGroup != null || allExpanded ? g.width : g.minifiedWidth;
package/src/org/parser.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import type { PaletteColors } from '../palettes';
2
2
  import type { DgmoError } from '../diagnostics';
3
3
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
4
- import type { TagGroup, TagEntry } from '../utils/tag-groups';
5
- import { isTagBlockHeading, matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
4
+ import type { TagGroup } from '../utils/tag-groups';
5
+ import {
6
+ isTagBlockHeading,
7
+ matchTagBlockHeading,
8
+ validateTagValues,
9
+ } from '../utils/tag-groups';
6
10
  import {
7
11
  measureIndent,
8
12
  extractColor,
9
13
  parsePipeMetadata,
10
- MULTIPLE_PIPE_WARNING,
14
+ MULTIPLE_PIPE_ERROR,
11
15
  parseFirstLine,
12
16
  OPTION_NOCOLON_RE,
13
17
  } from '../utils/parsing';
@@ -46,18 +50,18 @@ const METADATA_RE = /^([^:]+):\s*(.+)$/;
46
50
 
47
51
  /** Known org chart options (key-value). */
48
52
  const KNOWN_OPTIONS = new Set([
49
- 'direction', 'sub-node-label', 'hide', 'show-sub-node-count',
50
- ]);
51
- /** Known org chart boolean options (bare keyword = on). */
52
- const KNOWN_BOOLEANS = new Set([
53
+ 'sub-node-label',
54
+ 'hide',
53
55
  'show-sub-node-count',
54
56
  ]);
57
+ /** Known org chart boolean options (bare keyword = on). */
58
+ const KNOWN_BOOLEANS = new Set(['show-sub-node-count', 'direction-tb']);
55
59
 
56
60
  // ============================================================
57
61
  // Inference
58
62
  // ============================================================
59
63
 
60
- /** Returns true if content contains tag group headings (`tag: …` or `## …`), suggesting an org chart. */
64
+ /** Returns true if content contains tag group headings (`tag …`), suggesting an org chart. */
61
65
  export function looksLikeOrg(content: string): boolean {
62
66
  for (const line of content.split('\n')) {
63
67
  const trimmed = line.trim();
@@ -71,10 +75,7 @@ export function looksLikeOrg(content: string): boolean {
71
75
  // Parser
72
76
  // ============================================================
73
77
 
74
- export function parseOrg(
75
- content: string,
76
- palette?: PaletteColors
77
- ): ParsedOrg {
78
+ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
78
79
  const result: ParsedOrg = {
79
80
  title: null,
80
81
  titleLineNumber: null,
@@ -147,7 +148,22 @@ export function parseOrg(
147
148
  const firstLine = parseFirstLine(trimmed);
148
149
  if (firstLine) {
149
150
  if (firstLine.chartType !== 'org') {
150
- const allTypes = ['org', 'class', 'flowchart', 'sequence', 'er', 'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline', 'arc', 'slope'];
151
+ const allTypes = [
152
+ 'org',
153
+ 'class',
154
+ 'flowchart',
155
+ 'sequence',
156
+ 'er',
157
+ 'bar',
158
+ 'line',
159
+ 'pie',
160
+ 'scatter',
161
+ 'sankey',
162
+ 'venn',
163
+ 'timeline',
164
+ 'arc',
165
+ 'slope',
166
+ ];
151
167
  let msg = `Expected chart type "org", got "${firstLine.chartType}"`;
152
168
  const hint = suggest(firstLine.chartType, allTypes);
153
169
  if (hint) msg += `. ${hint}`;
@@ -169,10 +185,6 @@ export function parseOrg(
169
185
  pushError(lineNumber, 'Tag groups must appear before org content');
170
186
  continue;
171
187
  }
172
- if (tagBlockMatch.deprecated) {
173
- pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
174
- continue;
175
- }
176
188
  currentTagGroup = {
177
189
  name: tagBlockMatch.name,
178
190
  alias: tagBlockMatch.alias,
@@ -180,7 +192,10 @@ export function parseOrg(
180
192
  lineNumber,
181
193
  };
182
194
  if (tagBlockMatch.alias) {
183
- aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
195
+ aliasMap.set(
196
+ tagBlockMatch.alias.toLowerCase(),
197
+ tagBlockMatch.name.toLowerCase()
198
+ );
184
199
  }
185
200
  result.tagGroups.push(currentTagGroup);
186
201
  continue;
@@ -211,7 +226,10 @@ export function parseOrg(
211
226
  if (indent > 0) {
212
227
  const { label, color } = extractColor(trimmed, palette);
213
228
  if (!color) {
214
- pushError(lineNumber, `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`);
229
+ pushError(
230
+ lineNumber,
231
+ `Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
232
+ );
215
233
  continue;
216
234
  }
217
235
  // First entry is the default
@@ -226,7 +244,7 @@ export function parseOrg(
226
244
  continue;
227
245
  }
228
246
  // Non-indented line after tag group — fall through to content parsing
229
- currentTagGroup = null;
247
+ currentTagGroup = null; // eslint-disable-line no-useless-assignment
230
248
  }
231
249
 
232
250
  // --- Org content phase ---
@@ -241,7 +259,9 @@ export function parseOrg(
241
259
  // Check for metadata syntax: key: value
242
260
  // Lines containing '|' are pipe-delimited nodes (e.g. "Alice | role: Engineer"),
243
261
  // not metadata — skip the metadata regex for them.
244
- const metadataMatch = trimmed.includes('|') ? null : trimmed.match(METADATA_RE);
262
+ const metadataMatch = trimmed.includes('|')
263
+ ? null
264
+ : trimmed.match(METADATA_RE);
245
265
 
246
266
  if (containerMatch) {
247
267
  // It's a container node
@@ -280,14 +300,30 @@ export function parseOrg(
280
300
  // Otherwise it's an orphan metadata error
281
301
  if (indent === 0) {
282
302
  // Treat as a node label (e.g., "Dr. Smith: Surgeon" is a valid name)
283
- const node = parseNodeLabel(trimmed, indent, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
303
+ const node = parseNodeLabel(
304
+ trimmed,
305
+ indent,
306
+ lineNumber,
307
+ palette,
308
+ ++nodeCounter,
309
+ aliasMap,
310
+ pushWarning
311
+ );
284
312
  attachNode(node, indent, indentStack, result);
285
313
  } else {
286
314
  pushError(lineNumber, 'Metadata has no parent node');
287
315
  }
288
316
  } else {
289
317
  // It's a node label — possibly with single-line pipe-delimited metadata
290
- const node = parseNodeLabel(trimmed, indent, lineNumber, palette, ++nodeCounter, aliasMap, pushWarning);
318
+ const node = parseNodeLabel(
319
+ trimmed,
320
+ indent,
321
+ lineNumber,
322
+ palette,
323
+ ++nodeCounter,
324
+ aliasMap,
325
+ pushWarning
326
+ );
291
327
  attachNode(node, indent, indentStack, result);
292
328
  }
293
329
  }
@@ -307,7 +343,11 @@ export function parseOrg(
307
343
  validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
308
344
  }
309
345
 
310
- if (result.roots.length === 0 && result.tagGroups.length === 0 && !result.error) {
346
+ if (
347
+ result.roots.length === 0 &&
348
+ result.tagGroups.length === 0 &&
349
+ !result.error
350
+ ) {
311
351
  const diag = makeDgmoError(1, 'No nodes found in org chart');
312
352
  result.diagnostics.push(diag);
313
353
  result.error = formatDgmoError(diag);
@@ -327,15 +367,19 @@ function parseNodeLabel(
327
367
  palette: PaletteColors | undefined,
328
368
  counter: number,
329
369
  aliasMap: Map<string, string> = new Map(),
330
- warnFn?: (line: number, msg: string) => void,
370
+ warnFn?: (line: number, msg: string) => void
331
371
  ): OrgNode {
332
372
  // Check for single-line compact metadata: "Alice Park | role: Senior, location: NY"
333
373
  const segments = trimmed.split('|').map((s) => s.trim());
334
374
 
335
- let rawLabel = segments[0];
375
+ const rawLabel = segments[0];
336
376
  const { label, color } = extractColor(rawLabel, palette);
337
377
 
338
- const metadata = parsePipeMetadata(segments, aliasMap, warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_WARNING) : undefined);
378
+ const metadata = parsePipeMetadata(
379
+ segments,
380
+ aliasMap,
381
+ warnFn ? () => warnFn(lineNumber, MULTIPLE_PIPE_ERROR) : undefined
382
+ );
339
383
 
340
384
  return {
341
385
  id: `node-${counter}`,
@@ -36,14 +36,14 @@ export interface ResolveImportsResult {
36
36
  const MAX_DEPTH = 10;
37
37
  const IMPORT_RE = /^(\s+)import:?\s+(.+\.dgmo)\s*$/i;
38
38
  const TAGS_RE = /^tags:?\s+(.+\.dgmo)\s*$/i;
39
- /** Matches new-style first line: `org ...` or old `chart: ...` or `title: ...` */
40
- const HEADER_RE = /^(org|kanban|chart\s*:|title\s*:)/i;
39
+ /** Matches new-style first line: `org ...` or `kanban ...` or `title: ...` */
40
+ const HEADER_RE = /^(org|kanban|title\s*:)/i;
41
41
  /**
42
42
  * Known option keys that can appear in org chart headers (space-separated).
43
43
  * Only these are stripped from imported files — avoids eating content like "Alice Chen".
44
44
  */
45
45
  const KNOWN_HEADER_OPTIONS = new Set([
46
- 'direction', 'sub-node-label', 'hide', 'show-sub-node-count',
46
+ 'direction-tb', 'sub-node-label', 'hide', 'show-sub-node-count',
47
47
  'color-off',
48
48
  ]);
49
49
 
@@ -25,14 +25,15 @@ export {
25
25
  // Re-export palette definitions (alphabetical)
26
26
  export { boldPalette } from './bold';
27
27
  export { catppuccinPalette } from './catppuccin';
28
- export { draculaPalette } from './dracula';
29
28
  export { gruvboxPalette } from './gruvbox';
30
- export { monokaiPalette } from './monokai';
31
29
  export { nordPalette } from './nord';
32
30
  export { oneDarkPalette } from './one-dark';
33
31
  export { rosePinePalette } from './rose-pine';
34
32
  export { solarizedPalette } from './solarized';
35
33
  export { tokyoNightPalette } from './tokyo-night';
36
34
 
35
+ export { draculaPalette } from './dracula';
36
+ export { monokaiPalette } from './monokai';
37
+
37
38
  // Re-export Mermaid bridge
38
39
  export { buildMermaidThemeVars, buildThemeCSS } from './mermaid-bridge';
package/src/render.ts CHANGED
@@ -36,8 +36,7 @@ async function ensureDom(): Promise<void> {
36
36
  * ```ts
37
37
  * import { render } from '@diagrammo/dgmo';
38
38
  *
39
- * const svg = await render(`chart: pie
40
- * title: Languages
39
+ * const svg = await render(`pie Languages
41
40
  * TypeScript: 45
42
41
  * Python: 30
43
42
  * Rust: 25`);