@diagrammo/dgmo 0.2.27 → 0.2.28

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 (47) hide show
  1. package/.claude/skills/dgmo-chart/SKILL.md +107 -0
  2. package/.claude/skills/dgmo-flowchart/SKILL.md +61 -0
  3. package/.claude/skills/dgmo-generate/SKILL.md +58 -0
  4. package/.claude/skills/dgmo-sequence/SKILL.md +83 -0
  5. package/.cursorrules +117 -0
  6. package/.github/copilot-instructions.md +117 -0
  7. package/.windsurfrules +117 -0
  8. package/README.md +10 -3
  9. package/dist/cli.cjs +116 -108
  10. package/dist/index.cjs +543 -351
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +39 -24
  13. package/dist/index.d.ts +39 -24
  14. package/dist/index.js +540 -350
  15. package/dist/index.js.map +1 -1
  16. package/docs/ai-integration.md +125 -0
  17. package/docs/language-reference.md +784 -0
  18. package/package.json +10 -3
  19. package/src/c4/parser.ts +90 -74
  20. package/src/c4/renderer.ts +13 -12
  21. package/src/c4/types.ts +6 -4
  22. package/src/chart.ts +3 -2
  23. package/src/class/parser.ts +2 -10
  24. package/src/class/types.ts +1 -1
  25. package/src/cli.ts +130 -19
  26. package/src/d3.ts +1 -1
  27. package/src/dgmo-mermaid.ts +1 -1
  28. package/src/dgmo-router.ts +1 -1
  29. package/src/echarts.ts +33 -13
  30. package/src/er/parser.ts +34 -43
  31. package/src/er/types.ts +1 -1
  32. package/src/graph/flowchart-parser.ts +2 -25
  33. package/src/graph/types.ts +1 -1
  34. package/src/index.ts +5 -0
  35. package/src/initiative-status/parser.ts +36 -7
  36. package/src/initiative-status/types.ts +1 -1
  37. package/src/kanban/parser.ts +32 -53
  38. package/src/kanban/renderer.ts +9 -8
  39. package/src/kanban/types.ts +6 -14
  40. package/src/org/parser.ts +47 -87
  41. package/src/org/resolver.ts +11 -12
  42. package/src/sequence/parser.ts +97 -15
  43. package/src/sequence/renderer.ts +62 -69
  44. package/src/utils/arrows.ts +75 -0
  45. package/src/utils/inline-markdown.ts +75 -0
  46. package/src/utils/parsing.ts +67 -0
  47. package/src/utils/tag-groups.ts +76 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,7 +24,12 @@
24
24
  },
25
25
  "files": [
26
26
  "dist",
27
- "src"
27
+ "src",
28
+ "docs",
29
+ ".claude/skills",
30
+ ".github/copilot-instructions.md",
31
+ ".cursorrules",
32
+ ".windsurfrules"
28
33
  ],
29
34
  "sideEffects": false,
30
35
  "scripts": {
@@ -33,7 +38,8 @@
33
38
  "dev": "tsup --watch",
34
39
  "test": "vitest run",
35
40
  "test:watch": "vitest",
36
- "gallery": "pnpm build && node scripts/generate-gallery.mjs"
41
+ "gallery": "pnpm build && node scripts/generate-gallery.mjs",
42
+ "check:duplication": "jscpd ./src"
37
43
  },
38
44
  "dependencies": {
39
45
  "@dagrejs/dagre": "^2.0.4",
@@ -59,6 +65,7 @@
59
65
  "@types/d3-shape": "^3.1.7",
60
66
  "@types/dagre": "^0.7.53",
61
67
  "@types/jsdom": "^21.1.7",
68
+ "jscpd": "^4.0.8",
62
69
  "tsup": "^8.5.1",
63
70
  "typescript": "^5.7.3",
64
71
  "vitest": "^3.0.0"
package/src/c4/parser.ts CHANGED
@@ -2,12 +2,20 @@
2
2
  // C4 Architecture Diagram — Parser
3
3
  // ============================================================
4
4
 
5
- import { resolveColor } from '../colors';
6
5
  import type { PaletteColors } from '../palettes';
7
6
  import type { DgmoError } from '../diagnostics';
8
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
9
- import type { OrgTagGroup } from '../org/parser';
8
+ import type { TagGroup } from '../utils/tag-groups';
9
+ import { matchTagBlockHeading } from '../utils/tag-groups';
10
10
  import { inferParticipantType } from '../sequence/participant-inference';
11
+ import {
12
+ measureIndent,
13
+ extractColor,
14
+ parsePipeMetadata,
15
+ CHART_TYPE_RE,
16
+ TITLE_RE,
17
+ OPTION_RE,
18
+ } from '../utils/parsing';
11
19
  import type {
12
20
  ParsedC4,
13
21
  C4Element,
@@ -23,12 +31,6 @@ import type {
23
31
  // Regex patterns
24
32
  // ============================================================
25
33
 
26
- const CHART_TYPE_RE = /^chart\s*:\s*(.+)/i;
27
- const TITLE_RE = /^title\s*:\s*(.+)/i;
28
- const OPTION_RE = /^([a-z][a-z0-9-]*)\s*:\s*(.+)$/i;
29
- const GROUP_HEADING_RE =
30
- /^##\s+(.+?)(?:\s+alias\s+(\w+))?(?:\s*\(([^)]+)\))?\s*$/;
31
- const COLOR_SUFFIX_RE = /\(([^)]+)\)\s*$/;
32
34
  const CONTAINER_RE = /^\[([^\]]+)\]$/;
33
35
 
34
36
  /** Matches element declarations: `person Name`, `system Name | k: v` */
@@ -40,6 +42,12 @@ const IS_A_RE = /\s+is\s+a(?:n)?\s+(\w+)\s*$/i;
40
42
  /** Matches relationship arrows: `->`, `~>`, `<->`, `<~>` */
41
43
  const RELATIONSHIP_RE = /^(<?-?>|<?~?>)\s+(.+)$/;
42
44
 
45
+ /** Labeled arrow relationships: -label->, ~label~>, <-label->, <~label~> */
46
+ const C4_LABELED_SYNC_RE = /^-(.+)->\s+(.+)$/;
47
+ const C4_LABELED_ASYNC_RE = /^~(.+)~>\s+(.+)$/;
48
+ const C4_LABELED_BIDI_SYNC_RE = /^<-(.+)->\s+(.+)$/;
49
+ const C4_LABELED_BIDI_ASYNC_RE = /^<~(.+)~>\s+(.+)$/;
50
+
43
51
  /** Matches section headers: `containers:`, `components:`, `deployment:` */
44
52
  const SECTION_HEADER_RE = /^(containers|components|deployment)\s*:\s*$/i;
45
53
 
@@ -53,28 +61,6 @@ const METADATA_RE = /^([^:]+):\s*(.+)$/;
53
61
  // Helpers
54
62
  // ============================================================
55
63
 
56
- function measureIndent(line: string): number {
57
- let indent = 0;
58
- for (const ch of line) {
59
- if (ch === ' ') indent++;
60
- else if (ch === '\t') indent += 4;
61
- else break;
62
- }
63
- return indent;
64
- }
65
-
66
- function extractColor(
67
- label: string,
68
- palette?: PaletteColors,
69
- ): { label: string; color?: string } {
70
- const m = label.match(COLOR_SUFFIX_RE);
71
- if (!m) return { label };
72
- const colorName = m[1].trim();
73
- return {
74
- label: label.substring(0, m.index!).trim(),
75
- color: resolveColor(colorName, palette),
76
- };
77
- }
78
64
 
79
65
  const VALID_ELEMENT_TYPES = new Set<string>([
80
66
  'person',
@@ -186,27 +172,6 @@ function parseRelationshipBody(
186
172
  return { target, label: rest };
187
173
  }
188
174
 
189
- /** Parse pipe-delimited metadata from segments after the first (name) segment. */
190
- function parsePipeMetadata(
191
- segments: string[],
192
- aliasMap: Map<string, string>,
193
- ): Record<string, string> {
194
- const metadata: Record<string, string> = {};
195
- for (let j = 1; j < segments.length; j++) {
196
- for (const part of segments[j].split(',')) {
197
- const trimmedPart = part.trim();
198
- if (!trimmedPart) continue;
199
- const colonIdx = trimmedPart.indexOf(':');
200
- if (colonIdx > 0) {
201
- const rawKey = trimmedPart.substring(0, colonIdx).trim().toLowerCase();
202
- const key = aliasMap.get(rawKey) ?? rawKey;
203
- const value = trimmedPart.substring(colonIdx + 1).trim();
204
- metadata[key] = value;
205
- }
206
- }
207
- }
208
- return metadata;
209
- }
210
175
 
211
176
  // ============================================================
212
177
  // Stack entry types
@@ -287,7 +252,7 @@ export function parseC4(
287
252
  let inDeployment = false;
288
253
 
289
254
  // Tag group parsing state
290
- let currentTagGroup: OrgTagGroup | null = null;
255
+ let currentTagGroup: TagGroup | null = null;
291
256
  const aliasMap = new Map<string, string>();
292
257
 
293
258
  // Name uniqueness tracking
@@ -341,40 +306,42 @@ export function parseC4(
341
306
  }
342
307
  }
343
308
 
344
- // Generic header options
345
- if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
346
- const optMatch = trimmed.match(OPTION_RE);
347
- if (optMatch && !trimmed.startsWith('##')) {
348
- const key = optMatch[1].trim().toLowerCase();
349
- if (key !== 'chart' && key !== 'title') {
350
- result.options[key] = optMatch[2].trim();
351
- continue;
352
- }
353
- }
354
- }
355
-
356
- // ## Tag group heading
357
- const groupMatch = trimmed.match(GROUP_HEADING_RE);
358
- if (groupMatch) {
309
+ // Tag group heading — `tag: Name` (new) or `## Name` (deprecated)
310
+ // Must be checked BEFORE OPTION_RE to prevent `tag: Rank` being swallowed as option
311
+ const tagBlockMatch = matchTagBlockHeading(trimmed);
312
+ if (tagBlockMatch) {
359
313
  if (contentStarted) {
360
- pushError(lineNumber, 'Tag groups (##) must appear before content');
314
+ pushError(lineNumber, 'Tag groups must appear before content');
361
315
  continue;
362
316
  }
363
- const groupName = groupMatch[1].trim();
364
- const alias = groupMatch[2] || undefined;
317
+ if (tagBlockMatch.deprecated) {
318
+ pushError(lineNumber, `'## ${tagBlockMatch.name}' is deprecated for tag groups — use 'tag: ${tagBlockMatch.name}' instead`, 'warning');
319
+ }
365
320
  currentTagGroup = {
366
- name: groupName,
367
- alias,
321
+ name: tagBlockMatch.name,
322
+ alias: tagBlockMatch.alias,
368
323
  entries: [],
369
324
  lineNumber,
370
325
  };
371
- if (alias) {
372
- aliasMap.set(alias.toLowerCase(), groupName.toLowerCase());
326
+ if (tagBlockMatch.alias) {
327
+ aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
373
328
  }
374
329
  result.tagGroups.push(currentTagGroup);
375
330
  continue;
376
331
  }
377
332
 
333
+ // Generic header options
334
+ if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
335
+ const optMatch = trimmed.match(OPTION_RE);
336
+ if (optMatch) {
337
+ const key = optMatch[1].trim().toLowerCase();
338
+ if (key !== 'chart' && key !== 'title') {
339
+ result.options[key] = optMatch[2].trim();
340
+ continue;
341
+ }
342
+ }
343
+ }
344
+
378
345
  // Tag group entries
379
346
  if (currentTagGroup && !contentStarted) {
380
347
  const indent = measureIndent(line);
@@ -529,6 +496,55 @@ export function parseC4(
529
496
  continue;
530
497
  }
531
498
 
499
+ // ── Labeled arrow relationships: -label->, ~label~>, <-label->, <~label~> ──
500
+ // Must be checked BEFORE plain RELATIONSHIP_RE to avoid partial matches
501
+ {
502
+ const labeledPatterns: {
503
+ re: RegExp;
504
+ arrowType: C4ArrowType;
505
+ }[] = [
506
+ { re: C4_LABELED_BIDI_SYNC_RE, arrowType: 'bidirectional' },
507
+ { re: C4_LABELED_BIDI_ASYNC_RE, arrowType: 'bidirectional-async' },
508
+ { re: C4_LABELED_SYNC_RE, arrowType: 'sync' },
509
+ { re: C4_LABELED_ASYNC_RE, arrowType: 'async' },
510
+ ];
511
+ let labeledHandled = false;
512
+ for (const { re, arrowType } of labeledPatterns) {
513
+ const m = trimmed.match(re);
514
+ if (!m) continue;
515
+ const rawLabel = m[1].trim();
516
+ const targetBody = m[2].trim();
517
+ if (!rawLabel) break; // empty label — fall through to plain arrow
518
+
519
+ // Extract [technology] from end of label
520
+ let label: string | undefined = rawLabel;
521
+ let technology: string | undefined;
522
+ const techMatch = rawLabel.match(/\[([^\]]+)\]\s*$/);
523
+ if (techMatch) {
524
+ label = rawLabel.substring(0, techMatch.index!).trim() || undefined;
525
+ technology = techMatch[1].trim();
526
+ }
527
+
528
+ const rel: C4Relationship = {
529
+ target: targetBody,
530
+ label,
531
+ technology,
532
+ arrowType,
533
+ lineNumber,
534
+ };
535
+
536
+ const parentEntry = findParentElement(indent, stack);
537
+ if (parentEntry) {
538
+ parentEntry.element.relationships.push(rel);
539
+ } else {
540
+ result.relationships.push(rel);
541
+ }
542
+ labeledHandled = true;
543
+ break;
544
+ }
545
+ if (labeledHandled) continue;
546
+ }
547
+
532
548
  // ── Relationships ───────────────────────────────────────
533
549
  const relMatch = trimmed.match(RELATIONSHIP_RE);
534
550
  if (relMatch) {
@@ -6,6 +6,7 @@ import * as d3Selection from 'd3-selection';
6
6
  import * as d3Shape from 'd3-shape';
7
7
  import { FONT_FAMILY } from '../fonts';
8
8
  import type { PaletteColors } from '../palettes';
9
+ import { renderInlineText } from '../utils/inline-markdown';
9
10
  import type { ParsedC4 } from './types';
10
11
  import type { C4Shape } from './types';
11
12
  import type { C4LayoutResult, C4LayoutNode, C4LayoutEdge, C4LayoutBoundary } from './layout';
@@ -546,20 +547,20 @@ export function renderC4Context(
546
547
 
547
548
  yPos += DIVIDER_GAP;
548
549
 
549
- // Description (wrapping, muted)
550
+ // Description (wrapping, muted, inline markdown)
550
551
  if (node.description) {
551
552
  const contentWidth = w - CARD_H_PAD * 2;
552
553
  const lines = wrapText(node.description, contentWidth, DESC_CHAR_WIDTH);
553
554
  for (const line of lines) {
554
- nodeG
555
+ const textEl = nodeG
555
556
  .append('text')
556
557
  .attr('x', 0)
557
558
  .attr('y', yPos + DESC_FONT_SIZE / 2)
558
559
  .attr('text-anchor', 'middle')
559
560
  .attr('dominant-baseline', 'central')
560
561
  .attr('fill', palette.textMuted)
561
- .attr('font-size', DESC_FONT_SIZE)
562
- .text(line);
562
+ .attr('font-size', DESC_FONT_SIZE);
563
+ renderInlineText(textEl, line, palette, DESC_FONT_SIZE);
563
564
  yPos += DESC_LINE_HEIGHT;
564
565
  }
565
566
  }
@@ -1617,20 +1618,20 @@ export function renderC4Containers(
1617
1618
  if (node.type === 'container') {
1618
1619
  // Container cards: description above divider, metadata below
1619
1620
 
1620
- // Description (above divider)
1621
+ // Description (above divider, inline markdown)
1621
1622
  if (node.description) {
1622
1623
  const contentWidth = w - CARD_H_PAD * 2;
1623
1624
  const lines = wrapText(node.description, contentWidth, DESC_CHAR_WIDTH);
1624
1625
  for (const line of lines) {
1625
- nodeG
1626
+ const textEl = nodeG
1626
1627
  .append('text')
1627
1628
  .attr('x', 0)
1628
1629
  .attr('y', yPos + DESC_FONT_SIZE / 2)
1629
1630
  .attr('text-anchor', 'middle')
1630
1631
  .attr('dominant-baseline', 'central')
1631
1632
  .attr('fill', palette.textMuted)
1632
- .attr('font-size', DESC_FONT_SIZE)
1633
- .text(line);
1633
+ .attr('font-size', DESC_FONT_SIZE);
1634
+ renderInlineText(textEl, line, palette, DESC_FONT_SIZE);
1634
1635
  yPos += DESC_LINE_HEIGHT;
1635
1636
  }
1636
1637
  }
@@ -1696,20 +1697,20 @@ export function renderC4Containers(
1696
1697
 
1697
1698
  yPos += DIVIDER_GAP;
1698
1699
 
1699
- // Description
1700
+ // Description (inline markdown)
1700
1701
  if (node.description) {
1701
1702
  const contentWidth = w - CARD_H_PAD * 2;
1702
1703
  const lines = wrapText(node.description, contentWidth, DESC_CHAR_WIDTH);
1703
1704
  for (const line of lines) {
1704
- nodeG
1705
+ const textEl = nodeG
1705
1706
  .append('text')
1706
1707
  .attr('x', 0)
1707
1708
  .attr('y', yPos + DESC_FONT_SIZE / 2)
1708
1709
  .attr('text-anchor', 'middle')
1709
1710
  .attr('dominant-baseline', 'central')
1710
1711
  .attr('fill', palette.textMuted)
1711
- .attr('font-size', DESC_FONT_SIZE)
1712
- .text(line);
1712
+ .attr('font-size', DESC_FONT_SIZE);
1713
+ renderInlineText(textEl, line, palette, DESC_FONT_SIZE);
1713
1714
  yPos += DESC_LINE_HEIGHT;
1714
1715
  }
1715
1716
  }
package/src/c4/types.ts CHANGED
@@ -2,11 +2,13 @@
2
2
  // C4 Architecture Diagram — Types
3
3
  // ============================================================
4
4
 
5
- import type { OrgTagGroup, OrgTagEntry } from '../org/parser';
5
+ import type { TagGroup, TagEntry } from '../utils/tag-groups';
6
6
  import type { DgmoError } from '../diagnostics';
7
7
 
8
- // Re-export tag types for convenience
9
- export type { OrgTagGroup as C4TagGroup, OrgTagEntry as C4TagEntry };
8
+ /** @deprecated Use `TagEntry` from `utils/tag-groups` */
9
+ export type C4TagEntry = TagEntry;
10
+ /** @deprecated Use `TagGroup` from `utils/tag-groups` */
11
+ export type C4TagGroup = TagGroup;
10
12
 
11
13
  // ── String unions ────────────────────────────────────────────
12
14
 
@@ -77,7 +79,7 @@ export interface ParsedC4 {
77
79
  title: string | null;
78
80
  titleLineNumber: number | null;
79
81
  options: Record<string, string>;
80
- tagGroups: OrgTagGroup[];
82
+ tagGroups: TagGroup[];
81
83
  elements: C4Element[];
82
84
  relationships: C4Relationship[];
83
85
  deployment: C4DeploymentNode[];
package/src/chart.ts CHANGED
@@ -37,7 +37,7 @@ export interface ParsedChart {
37
37
  labels?: 'name' | 'value' | 'percent' | 'full';
38
38
  data: ChartDataPoint[];
39
39
  diagnostics: DgmoError[];
40
- error?: string;
40
+ error: string | null;
41
41
  }
42
42
 
43
43
  // ============================================================
@@ -90,6 +90,7 @@ export function parseChart(
90
90
  type: 'bar',
91
91
  data: [],
92
92
  diagnostics: [],
93
+ error: null,
93
94
  };
94
95
 
95
96
  const fail = (line: number, message: string): ParsedChart => {
@@ -110,7 +111,7 @@ export function parseChart(
110
111
  if (/^#{2,}\s+/.test(trimmed)) continue;
111
112
 
112
113
  // Skip comments
113
- if (trimmed.startsWith('#') || trimmed.startsWith('//')) continue;
114
+ if (trimmed.startsWith('//')) continue;
114
115
 
115
116
  // Parse key: value pairs
116
117
  const colonIndex = trimmed.indexOf(':');
@@ -1,6 +1,7 @@
1
1
  import { resolveColor } from '../colors';
2
2
  import type { PaletteColors } from '../palettes';
3
3
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
4
+ import { measureIndent } from '../utils/parsing';
4
5
  import type {
5
6
  ParsedClassDiagram,
6
7
  ClassNode,
@@ -15,16 +16,6 @@ import type {
15
16
  // Helpers
16
17
  // ============================================================
17
18
 
18
- function measureIndent(line: string): number {
19
- let indent = 0;
20
- for (const ch of line) {
21
- if (ch === ' ') indent++;
22
- else if (ch === '\t') indent += 4;
23
- else break;
24
- }
25
- return indent;
26
- }
27
-
28
19
  function classId(name: string): string {
29
20
  return name.toLowerCase().trim();
30
21
  }
@@ -172,6 +163,7 @@ export function parseClassDiagram(
172
163
  relationships: [],
173
164
  options: {},
174
165
  diagnostics: [],
166
+ error: null,
175
167
  };
176
168
 
177
169
  const fail = (line: number, message: string): ParsedClassDiagram => {
@@ -51,5 +51,5 @@ export interface ParsedClassDiagram {
51
51
  relationships: ClassRelationship[];
52
52
  options: Record<string, string>;
53
53
  diagnostics: DgmoError[];
54
- error?: string;
54
+ error: string | null;
55
55
  }