@diagrammo/dgmo 0.6.1 → 0.6.3

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 (52) hide show
  1. package/.claude/commands/dgmo.md +294 -0
  2. package/AGENTS.md +148 -0
  3. package/dist/cli.cjs +338 -163
  4. package/dist/index.cjs +1080 -319
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +28 -4
  7. package/dist/index.d.ts +28 -4
  8. package/dist/index.js +1078 -319
  9. package/dist/index.js.map +1 -1
  10. package/docs/ai-integration.md +33 -50
  11. package/package.json +8 -5
  12. package/src/c4/layout.ts +68 -10
  13. package/src/c4/parser.ts +0 -16
  14. package/src/c4/renderer.ts +1 -5
  15. package/src/class/layout.ts +0 -1
  16. package/src/class/parser.ts +28 -0
  17. package/src/class/renderer.ts +5 -26
  18. package/src/cli.ts +673 -2
  19. package/src/completion.ts +58 -0
  20. package/src/d3.ts +58 -106
  21. package/src/dgmo-router.ts +0 -57
  22. package/src/echarts.ts +96 -55
  23. package/src/er/classify.ts +206 -0
  24. package/src/er/layout.ts +259 -94
  25. package/src/er/parser.ts +30 -1
  26. package/src/er/renderer.ts +231 -18
  27. package/src/graph/flowchart-parser.ts +27 -4
  28. package/src/graph/flowchart-renderer.ts +1 -2
  29. package/src/graph/state-parser.ts +0 -1
  30. package/src/graph/state-renderer.ts +1 -3
  31. package/src/index.ts +10 -0
  32. package/src/infra/compute.ts +0 -7
  33. package/src/infra/layout.ts +60 -15
  34. package/src/infra/parser.ts +46 -4
  35. package/src/infra/renderer.ts +376 -47
  36. package/src/initiative-status/layout.ts +46 -30
  37. package/src/initiative-status/renderer.ts +5 -25
  38. package/src/kanban/parser.ts +0 -2
  39. package/src/org/layout.ts +0 -4
  40. package/src/org/renderer.ts +7 -28
  41. package/src/sequence/parser.ts +14 -11
  42. package/src/sequence/renderer.ts +0 -2
  43. package/src/sequence/tag-resolution.ts +0 -1
  44. package/src/sitemap/layout.ts +1 -14
  45. package/src/sitemap/parser.ts +1 -2
  46. package/src/sitemap/renderer.ts +0 -3
  47. package/src/utils/arrows.ts +7 -7
  48. package/src/utils/export-container.ts +40 -0
  49. package/.claude/skills/dgmo-chart/SKILL.md +0 -141
  50. package/.claude/skills/dgmo-flowchart/SKILL.md +0 -61
  51. package/.claude/skills/dgmo-generate/SKILL.md +0 -59
  52. package/.claude/skills/dgmo-sequence/SKILL.md +0 -104
@@ -1,14 +1,16 @@
1
1
  # DGMO AI Integration Guide
2
2
 
3
- Use AI coding tools to generate `.dgmo` diagrams. This guide covers Claude Code, Copilot, Cursor, and other AI tools.
3
+ Use AI coding tools to generate `.dgmo` diagrams. This guide covers Claude Code, Copilot, Cursor, Windsurf, and any tool with an MCP client.
4
4
 
5
- ## MCP Server
5
+ ---
6
6
 
7
- `@diagrammo/dgmo-mcp` provides an MCP server that exposes DGMO rendering, sharing, and documentation tools. Works with Claude Desktop, Claude Code, and any MCP-compatible client.
7
+ ## MCP Server (recommended for Claude)
8
8
 
9
- 5 tools: `render_diagram`, `share_diagram`, `open_in_app`, `list_chart_types`, `get_language_reference`.
9
+ `@diagrammo/dgmo-mcp` provides an MCP server that gives Claude the ability to render, share, and look up DGMO diagrams directly — no file management needed.
10
10
 
11
- Setup (Claude Code add to `.claude/settings.local.json`):
11
+ **5 tools:** `render_diagram`, `share_diagram`, `open_in_app`, `list_chart_types`, `get_language_reference`
12
+
13
+ Add to `~/.claude/settings.json` (global) or `.claude/settings.local.json` (project):
12
14
 
13
15
  ```json
14
16
  {
@@ -23,60 +25,46 @@ Setup (Claude Code — add to `.claude/settings.local.json`):
23
25
 
24
26
  See `dgmo-mcp/README.md` for full configuration options.
25
27
 
26
- ## Claude Code — Skills
27
-
28
- Copy the `.claude/skills/dgmo-*` directories from this repo into your project's `.claude/skills/` directory. This gives you four slash commands:
28
+ ---
29
29
 
30
- | Command | What it does |
31
- |---------|-------------|
32
- | `/dgmo-generate <description>` | Picks the best diagram type automatically |
33
- | `/dgmo-sequence <flow>` | Generates a sequence diagram |
34
- | `/dgmo-flowchart <process>` | Generates a flowchart |
35
- | `/dgmo-chart <data description>` | Generates a data chart |
30
+ ## Claude Code Skill (slash command)
36
31
 
37
- ### Setup
32
+ Installs a `/dgmo` slash command that gives Claude full dgmo context — all chart types, CLI flags, workflow, and tips.
38
33
 
39
34
  ```bash
40
- # Copy skills into your project
41
- cp -r node_modules/@diagrammo/dgmo/.claude/skills/dgmo-* .claude/skills/
42
-
43
- # Or if dgmo is installed globally
44
- cp -r $(npm root -g)/@diagrammo/dgmo/.claude/skills/dgmo-* .claude/skills/
35
+ dgmo --install-claude-skill
45
36
  ```
46
37
 
47
- ### Usage examples
38
+ This copies a skill file into `~/.claude/commands/`, making `/dgmo` available in every Claude Code session.
48
39
 
49
- ```
50
- /dgmo-generate an ER diagram for a blog with users, posts, and comments
51
- /dgmo-sequence the OAuth2 authorization code flow
52
- /dgmo-flowchart CI/CD pipeline with build, test, and deploy stages
53
- /dgmo-chart quarterly revenue: Q1 100, Q2 120, Q3 110, Q4 130
54
- ```
40
+ ---
55
41
 
56
42
  ## Claude Code — CLAUDE.md snippet
57
43
 
58
- Add this to your project's `CLAUDE.md` to teach Claude about DGMO without installing skills:
44
+ To teach Claude about DGMO in a specific project without the global skill, add this to your `CLAUDE.md`:
59
45
 
60
46
  ```markdown
61
47
  ## DGMO Diagrams
62
48
 
63
- When the user asks for a diagram, generate a `.dgmo` file. DGMO is a text-based diagram language.
49
+ When the user asks for a diagram, generate a `.dgmo` file.
64
50
 
65
51
  Quick reference:
66
- - Sequence: `A -> B: message` or `A -message-> B`
52
+ - Sequence: `A -message-> B` or `A <-response- B`
67
53
  - Flowchart: `(Start) -> [Process] -> <Decision?> -yes-> (End)`
68
54
  - Bar chart: `chart: bar` then `Label: value` lines
69
55
  - ER diagram: `chart: er` then table definitions and `table1 1--* table2` relationships
70
56
  - Org chart: `chart: org` then indented hierarchy
71
57
 
72
- Full reference: see `node_modules/@diagrammo/dgmo/docs/language-reference.md`
58
+ Full reference: `node_modules/@diagrammo/dgmo/docs/language-reference.md`
73
59
 
74
- Render with: `dgmo file.dgmo -o output.svg` or `dgmo file.dgmo -o url` for shareable link.
60
+ Render with: `dgmo file.dgmo` (PNG) or `dgmo file.dgmo -o url` (shareable link).
75
61
  ```
76
62
 
63
+ ---
64
+
77
65
  ## Other AI Tools — Prompt Files
78
66
 
79
- DGMO ships prompt files for popular AI coding tools. These are included in the npm package:
67
+ DGMO ships context files for popular AI coding tools, included in the npm package and auto-loaded when present in a project root.
80
68
 
81
69
  | File | Tool | How it works |
82
70
  |------|------|-------------|
@@ -84,42 +72,37 @@ DGMO ships prompt files for popular AI coding tools. These are included in the n
84
72
  | `.cursorrules` | Cursor | Auto-loaded when present in project root |
85
73
  | `.windsurfrules` | Windsurf | Auto-loaded when present in project root |
86
74
 
87
- ### Setup
88
-
89
75
  Copy the relevant file into your project root:
90
76
 
91
77
  ```bash
92
- # From node_modules
78
+ # From node_modules (if installed as a dependency)
93
79
  cp node_modules/@diagrammo/dgmo/.cursorrules .
94
80
  cp node_modules/@diagrammo/dgmo/.windsurfrules .
95
81
  mkdir -p .github && cp node_modules/@diagrammo/dgmo/.github/copilot-instructions.md .github/
96
82
 
97
- # Or from global install
83
+ # From global npm install
98
84
  cp $(npm root -g)/@diagrammo/dgmo/.cursorrules .
99
85
  ```
100
86
 
101
- Each file contains a condensed DGMO syntax reference with examples for the most common diagram types, all 29 chart types listed, rendering commands, and common mistakes to avoid.
87
+ Each file contains a condensed DGMO syntax reference with examples, all chart types listed, rendering commands, and common mistakes to avoid.
102
88
 
103
- ## Rendering
89
+ ---
104
90
 
105
- If the `dgmo` CLI is installed, diagrams can be rendered:
91
+ ## Rendering commands
106
92
 
107
93
  ```bash
108
- # Install
109
- npm install -g @diagrammo/dgmo # or: brew install diagrammo/dgmo/dgmo
110
-
111
- # Render
112
94
  dgmo diagram.dgmo # PNG output
113
95
  dgmo diagram.dgmo -o output.svg # SVG output
114
- dgmo diagram.dgmo -o url # Shareable URL
115
-
116
- # AI-friendly JSON output
117
- dgmo diagram.dgmo -o output.svg --json
118
- dgmo --chart-types --json # List all chart types
96
+ dgmo diagram.dgmo -o url # Shareable diagrammo.app URL
97
+ dgmo diagram.dgmo -o url --copy # URL copied to clipboard
98
+ dgmo --chart-types # List all supported chart types
99
+ dgmo --chart-types --json # Machine-readable chart type list
119
100
  ```
120
101
 
102
+ ---
103
+
121
104
  ## Supported chart types
122
105
 
123
106
  Run `dgmo --chart-types` for the full list, or see `docs/language-reference.md`.
124
107
 
125
- 32 types: bar, line, area, pie, doughnut, radar, polar-area, bar-stacked, scatter, sankey, chord, function, heatmap, funnel, slope, wordcloud, arc, timeline, venn, quadrant, sequence, flowchart, state, class, er, org, kanban, c4, initiative-status, sitemap, infra.
108
+ 29 types: bar, line, area, multi-line, pie, doughnut, radar, polar-area, bar-stacked, scatter, sankey, chord, function, heatmap, funnel, slope, wordcloud, arc, timeline, venn, quadrant, sequence, flowchart, class, er, org, kanban, c4, initiative-status, infra.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,10 +26,11 @@
26
26
  "dist",
27
27
  "src",
28
28
  "docs",
29
- ".claude/skills",
29
+ ".claude/commands",
30
30
  ".github/copilot-instructions.md",
31
31
  ".cursorrules",
32
- ".windsurfrules"
32
+ ".windsurfrules",
33
+ "AGENTS.md"
33
34
  ],
34
35
  "sideEffects": false,
35
36
  "scripts": {
@@ -39,7 +40,9 @@
39
40
  "test": "vitest run",
40
41
  "test:watch": "vitest",
41
42
  "gallery": "pnpm build && node scripts/generate-gallery.mjs",
42
- "check:duplication": "jscpd ./src"
43
+ "check:duplication": "jscpd ./src",
44
+ "check:deadcode": "knip",
45
+ "postinstall": "node -e \"console.log('\\n💡 Claude Code user? Run: dgmo --install-claude-skill\\n')\""
43
46
  },
44
47
  "dependencies": {
45
48
  "@dagrejs/dagre": "^2.0.4",
@@ -63,9 +66,9 @@
63
66
  "@types/d3-scale": "^4.0.8",
64
67
  "@types/d3-selection": "^3.0.11",
65
68
  "@types/d3-shape": "^3.1.7",
66
- "@types/dagre": "^0.7.54",
67
69
  "@types/jsdom": "^28.0.0",
68
70
  "jscpd": "^4.0.8",
71
+ "knip": "^6.0.1",
69
72
  "tsup": "^8.5.1",
70
73
  "typescript": "^5.7.3",
71
74
  "vitest": "^4.0.18"
package/src/c4/layout.ts CHANGED
@@ -86,7 +86,6 @@ const DESC_LINE_HEIGHT = 16;
86
86
  const DESC_CHAR_WIDTH = 6.5;
87
87
  const CARD_V_PAD = 14;
88
88
  const CARD_H_PAD = 20;
89
- const TECH_LINE_HEIGHT = 16;
90
89
  const META_LINE_HEIGHT = 16;
91
90
  const META_CHAR_WIDTH = 6.5;
92
91
  const MARGIN = 40;
@@ -109,17 +108,31 @@ const LEGEND_CAPSULE_PAD = 4;
109
108
  // Post-Layout Crossing Reduction
110
109
  // ============================================================
111
110
 
111
+ interface NodeGeometry {
112
+ y: number;
113
+ width: number;
114
+ height: number;
115
+ }
116
+
117
+ // Large penalty per edge-node collision — dominates the distance term so the
118
+ // sifter strongly prefers orderings where edges don't pass through other nodes.
119
+ const EDGE_NODE_COLLISION_WEIGHT = 5000;
120
+
112
121
  /**
113
122
  * Compute penalty for an edge ordering. Uses degree-weighted edge distance:
114
123
  * long edges to high-degree nodes are penalized more than to low-degree nodes.
115
124
  * This places shared/important nodes closer to their neighbors, reducing
116
125
  * visual edge congestion.
117
126
  *
127
+ * When nodeGeometry is provided, also adds a heavy penalty for each case where
128
+ * a straight-line edge bounding box overlaps another node — driving the sifter
129
+ * to prefer orderings that avoid edge-node collisions.
118
130
  */
119
131
  function computeEdgePenalty(
120
132
  edgeList: { source: string; target: string }[],
121
133
  nodePositions: Map<string, number>,
122
- degrees: Map<string, number>
134
+ degrees: Map<string, number>,
135
+ nodeGeometry?: Map<string, NodeGeometry>
123
136
  ): number {
124
137
  let penalty = 0;
125
138
 
@@ -135,6 +148,48 @@ function computeEdgePenalty(
135
148
  penalty += dist * weight;
136
149
  }
137
150
 
151
+ // Edge-node collision penalty: for each edge A→B, check if any other node C
152
+ // has its bounding box inside the straight-line bounding box of the edge.
153
+ // Uses x from nodePositions (updated per permutation) and y/size from nodeGeometry.
154
+ if (nodeGeometry) {
155
+ for (const edge of edgeList) {
156
+ const geomA = nodeGeometry.get(edge.source);
157
+ const geomB = nodeGeometry.get(edge.target);
158
+ if (!geomA || !geomB) continue;
159
+
160
+ const ax = nodePositions.get(edge.source) ?? 0;
161
+ const bx = nodePositions.get(edge.target) ?? 0;
162
+ const ay = geomA.y;
163
+ const by = geomB.y;
164
+
165
+ // Skip edges within the same rank — they have no vertical span to check
166
+ if (ay === by) continue;
167
+
168
+ const edgeMinX = Math.min(ax, bx);
169
+ const edgeMaxX = Math.max(ax, bx);
170
+ const edgeMinY = Math.min(ay, by);
171
+ const edgeMaxY = Math.max(ay, by);
172
+
173
+ for (const [name, geomC] of nodeGeometry) {
174
+ if (name === edge.source || name === edge.target) continue;
175
+ const cx = nodePositions.get(name) ?? 0;
176
+ const cy = geomC.y;
177
+ const hw = geomC.width / 2;
178
+ const hh = geomC.height / 2;
179
+
180
+ // AABB overlap: node C's box intersects the edge's straight-line bounding box
181
+ if (
182
+ cx + hw > edgeMinX &&
183
+ cx - hw < edgeMaxX &&
184
+ cy + hh > edgeMinY &&
185
+ cy - hh < edgeMaxY
186
+ ) {
187
+ penalty += EDGE_NODE_COLLISION_WEIGHT;
188
+ }
189
+ }
190
+ }
191
+ }
192
+
138
193
  return penalty;
139
194
  }
140
195
 
@@ -160,6 +215,13 @@ function reduceCrossings(
160
215
  degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
161
216
  }
162
217
 
218
+ // Build geometry map for edge-node collision scoring
219
+ const nodeGeometry = new Map<string, NodeGeometry>();
220
+ for (const name of g.nodes()) {
221
+ const pos = g.node(name);
222
+ if (pos) nodeGeometry.set(name, { y: pos.y, width: pos.width, height: pos.height });
223
+ }
224
+
163
225
  // Group nodes by rank
164
226
  const rankMap = new Map<number, string[]>();
165
227
  for (const name of g.nodes()) {
@@ -216,7 +278,7 @@ function reduceCrossings(
216
278
  }
217
279
 
218
280
  // Current penalty
219
- const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees);
281
+ const currentPenalty = computeEdgePenalty(edgeList, basePositions, degrees, nodeGeometry);
220
282
 
221
283
  // Try permutations (feasible for partition sizes ≤ 8)
222
284
  let bestPerm = [...partition];
@@ -229,7 +291,7 @@ function reduceCrossings(
229
291
  for (let i = 0; i < perm.length; i++) {
230
292
  testPositions.set(perm[i]!, xSlots[i]!);
231
293
  }
232
- const penalty = computeEdgePenalty(edgeList, testPositions, degrees);
294
+ const penalty = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
233
295
  if (penalty < bestPenalty) {
234
296
  bestPenalty = penalty;
235
297
  bestPerm = [...perm];
@@ -248,14 +310,14 @@ function reduceCrossings(
248
310
  for (let k = 0; k < workingOrder.length; k++) {
249
311
  testPositions.set(workingOrder[k]!, xSlots[k]!);
250
312
  }
251
- const before = computeEdgePenalty(edgeList, testPositions, degrees);
313
+ const before = computeEdgePenalty(edgeList, testPositions, degrees, nodeGeometry);
252
314
 
253
315
  [workingOrder[i], workingOrder[i + 1]] = [workingOrder[i + 1]!, workingOrder[i]!];
254
316
  const testPositions2 = new Map(basePositions);
255
317
  for (let k = 0; k < workingOrder.length; k++) {
256
318
  testPositions2.set(workingOrder[k]!, xSlots[k]!);
257
319
  }
258
- const after = computeEdgePenalty(edgeList, testPositions2, degrees);
320
+ const after = computeEdgePenalty(edgeList, testPositions2, degrees, nodeGeometry);
259
321
 
260
322
  if (after < before) {
261
323
  improved = true;
@@ -415,10 +477,6 @@ export function rollUpContextRelationships(parsed: ParsedC4): ContextRelationshi
415
477
  const allRels = collectAllRelationships(parsed.elements, ownerMap);
416
478
 
417
479
  // Also include orphan relationships
418
- for (const rel of parsed.relationships) {
419
- // Orphan rels have no source element name — skip them for context roll-up
420
- }
421
-
422
480
  // Separate system-level (explicit) from nested (rolled-up)
423
481
  const topLevelNames = new Set(parsed.elements.map((e) => e.name));
424
482
  const explicitKeys = new Set<string>();
package/src/c4/parser.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  // ============================================================
4
4
 
5
5
  import type { PaletteColors } from '../palettes';
6
- import type { DgmoError } from '../diagnostics';
7
6
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
8
7
  import type { TagGroup } from '../utils/tag-groups';
9
8
  import { matchTagBlockHeading } from '../utils/tag-groups';
@@ -751,21 +750,6 @@ function attachElement(
751
750
  // Post-parse validation
752
751
  // ============================================================
753
752
 
754
- function collectAllNames(result: ParsedC4): Map<string, number> {
755
- const names = new Map<string, number>();
756
- function walk(elements: C4Element[]) {
757
- for (const el of elements) {
758
- names.set(el.name.toLowerCase(), el.lineNumber);
759
- walk(el.children);
760
- for (const g of el.groups) {
761
- walk(g.children);
762
- }
763
- }
764
- }
765
- walk(result.elements);
766
- return names;
767
- }
768
-
769
753
  function validateRelationshipTargets(
770
754
  result: ParsedC4,
771
755
  knownNames: Map<string, number>,
@@ -9,8 +9,7 @@ import type { PaletteColors } from '../palettes';
9
9
  import { mix } from '../palettes/color-utils';
10
10
  import { renderInlineText } from '../utils/inline-markdown';
11
11
  import type { ParsedC4 } from './types';
12
- import type { C4Shape } from './types';
13
- import type { C4LayoutResult, C4LayoutNode, C4LayoutEdge, C4LayoutBoundary, C4LegendGroup } from './layout';
12
+ import type { C4LayoutResult, C4LayoutEdge, C4LegendGroup } from './layout';
14
13
  import { parseC4 } from './parser';
15
14
  import { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deployment, collectCardMetadata } from './layout';
16
15
  import {
@@ -50,7 +49,6 @@ const CARD_V_PAD = 14;
50
49
  const TYPE_LABEL_HEIGHT = 18;
51
50
  const DIVIDER_GAP = 6;
52
51
  const NAME_HEIGHT = 20;
53
- const TECH_LINE_HEIGHT = 16;
54
52
  const META_FONT_SIZE = 11;
55
53
  const META_CHAR_WIDTH = 6.5;
56
54
  const META_LINE_HEIGHT = 16;
@@ -257,7 +255,6 @@ export function renderC4Context(
257
255
  const scale = Math.min(MAX_SCALE, scaleX, scaleY);
258
256
 
259
257
  const scaledW = diagramW * scale;
260
- const scaledH = diagramH * scale;
261
258
  const offsetX = (width - scaledW) / 2;
262
259
  const offsetY = titleHeight + DIAGRAM_PADDING;
263
260
 
@@ -1300,7 +1297,6 @@ export function renderC4Containers(
1300
1297
  const scale = Math.min(MAX_SCALE, scaleX, scaleY);
1301
1298
 
1302
1299
  const scaledW = diagramW * scale;
1303
- const scaledH = diagramH * scale;
1304
1300
  const offsetX = (width - scaledW) / 2;
1305
1301
  const offsetY = titleHeight + DIAGRAM_PADDING;
1306
1302
 
@@ -2,7 +2,6 @@ import dagre from '@dagrejs/dagre';
2
2
  import type {
3
3
  ParsedClassDiagram,
4
4
  ClassNode,
5
- ClassRelationship,
6
5
  RelationshipType,
7
6
  } from './types';
8
7
 
@@ -383,3 +383,31 @@ export function looksLikeClassDiagram(content: string): boolean {
383
383
 
384
384
  return false;
385
385
  }
386
+
387
+ // ============================================================
388
+ // Symbol extraction (for completion API)
389
+ // ============================================================
390
+
391
+ import type { DiagramSymbols } from '../completion';
392
+
393
+ /**
394
+ * Extract class names (entities) from class diagram document text.
395
+ * Used by the dgmo completion API for ghost hints and popup completions.
396
+ */
397
+ export function extractSymbols(docText: string): DiagramSymbols {
398
+ const entities: string[] = [];
399
+ let inMetadata = true;
400
+ for (const rawLine of docText.split('\n')) {
401
+ const line = rawLine.trim();
402
+ if (inMetadata && /^[a-z-]+\s*:/i.test(line)) continue;
403
+ inMetadata = false;
404
+ if (line.length === 0 || /^\s/.test(rawLine)) continue;
405
+ const m = CLASS_DECL_RE.exec(line);
406
+ if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
407
+ }
408
+ return {
409
+ kind: 'class',
410
+ entities,
411
+ keywords: ['extends', 'implements', 'abstract', 'interface', 'enum'],
412
+ };
413
+ }
@@ -5,10 +5,11 @@
5
5
  import * as d3Selection from 'd3-selection';
6
6
  import * as d3Shape from 'd3-shape';
7
7
  import { FONT_FAMILY } from '../fonts';
8
+ import { runInExportContainer, extractExportSvg } from '../utils/export-container';
8
9
  import type { PaletteColors } from '../palettes';
9
10
  import { mix } from '../palettes/color-utils';
10
11
  import type { ParsedClassDiagram, ClassModifier, RelationshipType } from './types';
11
- import type { ClassLayoutResult, ClassLayoutNode, ClassLayoutEdge } from './layout';
12
+ import type { ClassLayoutResult } from './layout';
12
13
  import { parseClassDiagram } from './parser';
13
14
  import { layoutClassDiagram } from './layout';
14
15
 
@@ -99,8 +100,6 @@ function isSourceMarker(type: RelationshipType): boolean {
99
100
  // Main renderer
100
101
  // ============================================================
101
102
 
102
- type GSelection = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
103
-
104
103
  export function renderClassDiagram(
105
104
  container: HTMLDivElement,
106
105
  parsed: ParsedClassDiagram,
@@ -125,7 +124,6 @@ export function renderClassDiagram(
125
124
  const scale = Math.min(MAX_SCALE, scaleX, scaleY);
126
125
 
127
126
  const scaledW = diagramW * scale;
128
- const scaledH = diagramH * scale;
129
127
  const offsetX = (width - scaledW) / 2;
130
128
  const offsetY = titleHeight + DIAGRAM_PADDING;
131
129
 
@@ -506,16 +504,10 @@ export function renderClassDiagramForExport(
506
504
  const layout = layoutClassDiagram(parsed);
507
505
  const isDark = theme === 'dark';
508
506
 
509
- const container = document.createElement('div');
510
507
  const exportWidth = layout.width + DIAGRAM_PADDING * 2;
511
508
  const exportHeight = layout.height + DIAGRAM_PADDING * 2 + (parsed.title ? 40 : 0);
512
- container.style.width = `${exportWidth}px`;
513
- container.style.height = `${exportHeight}px`;
514
- container.style.position = 'absolute';
515
- container.style.left = '-9999px';
516
- document.body.appendChild(container);
517
509
 
518
- try {
510
+ return runInExportContainer(exportWidth, exportHeight, (container) => {
519
511
  renderClassDiagram(
520
512
  container,
521
513
  parsed,
@@ -525,19 +517,6 @@ export function renderClassDiagramForExport(
525
517
  undefined,
526
518
  { width: exportWidth, height: exportHeight }
527
519
  );
528
-
529
- const svgEl = container.querySelector('svg');
530
- if (!svgEl) return '';
531
-
532
- if (theme === 'transparent') {
533
- svgEl.style.background = 'none';
534
- }
535
-
536
- svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
537
- svgEl.style.fontFamily = FONT_FAMILY;
538
-
539
- return svgEl.outerHTML;
540
- } finally {
541
- document.body.removeChild(container);
542
- }
520
+ return extractExportSvg(container, theme);
521
+ });
543
522
  }