@diagrammo/dgmo 0.6.2 → 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 (44) hide show
  1. package/.claude/commands/dgmo.md +231 -13
  2. package/AGENTS.md +148 -0
  3. package/dist/cli.cjs +327 -153
  4. package/dist/index.cjs +305 -177
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +24 -3
  7. package/dist/index.d.ts +24 -3
  8. package/dist/index.js +303 -177
  9. package/dist/index.js.map +1 -1
  10. package/package.json +5 -3
  11. package/src/c4/layout.ts +0 -5
  12. package/src/c4/parser.ts +0 -16
  13. package/src/c4/renderer.ts +1 -5
  14. package/src/class/layout.ts +0 -1
  15. package/src/class/parser.ts +28 -0
  16. package/src/class/renderer.ts +5 -26
  17. package/src/cli.ts +563 -14
  18. package/src/completion.ts +58 -0
  19. package/src/d3.ts +58 -106
  20. package/src/dgmo-router.ts +0 -57
  21. package/src/echarts.ts +96 -55
  22. package/src/er/parser.ts +30 -1
  23. package/src/er/renderer.ts +1 -2
  24. package/src/graph/flowchart-parser.ts +27 -4
  25. package/src/graph/flowchart-renderer.ts +1 -2
  26. package/src/graph/state-parser.ts +0 -1
  27. package/src/graph/state-renderer.ts +1 -3
  28. package/src/index.ts +10 -0
  29. package/src/infra/compute.ts +0 -7
  30. package/src/infra/layout.ts +0 -2
  31. package/src/infra/parser.ts +46 -4
  32. package/src/infra/renderer.ts +1 -15
  33. package/src/initiative-status/renderer.ts +5 -25
  34. package/src/kanban/parser.ts +0 -2
  35. package/src/org/layout.ts +0 -4
  36. package/src/org/renderer.ts +7 -28
  37. package/src/sequence/parser.ts +14 -11
  38. package/src/sequence/renderer.ts +0 -2
  39. package/src/sequence/tag-resolution.ts +0 -1
  40. package/src/sitemap/layout.ts +1 -14
  41. package/src/sitemap/parser.ts +1 -2
  42. package/src/sitemap/renderer.ts +0 -3
  43. package/src/utils/arrows.ts +7 -7
  44. package/src/utils/export-container.ts +40 -0
@@ -7,12 +7,10 @@ import * as d3Shape from 'd3-shape';
7
7
  import { FONT_FAMILY } from '../fonts';
8
8
  import type { PaletteColors } from '../palettes';
9
9
  import { mix } from '../palettes/color-utils';
10
- import type { ParsedInfra, InfraTagGroup } from './types';
10
+ import type { InfraTagGroup } from './types';
11
11
  import { resolveColor } from '../colors';
12
- import type { ComputedInfraModel } from './types';
13
12
  import type { InfraLayoutResult, InfraLayoutNode, InfraLayoutEdge, InfraLayoutGroup } from './layout';
14
13
  import { inferRoles, collectDiagramRoles, collectFanoutSourceIds, FANOUT_ROLE } from './roles';
15
- import type { InfraRole } from './roles';
16
14
  import { parseInfra } from './parser';
17
15
  import { computeInfra } from './compute';
18
16
  import { layoutInfra } from './layout';
@@ -34,11 +32,9 @@ import {
34
32
  // Constants
35
33
  // ============================================================
36
34
 
37
- const DIAGRAM_PADDING = 20;
38
35
  const NODE_FONT_SIZE = 13;
39
36
  const META_FONT_SIZE = 10;
40
37
  const META_LINE_HEIGHT = 14;
41
- const RPS_FONT_SIZE = 11;
42
38
  const EDGE_LABEL_FONT_SIZE = 11;
43
39
  const GROUP_LABEL_FONT_SIZE = 14;
44
40
  const NODE_BORDER_RADIUS = 8;
@@ -58,8 +54,6 @@ const LEGEND_FIXED_GAP = 16; // gap between fixed legend and scaled diagram —
58
54
  const COLOR_HEALTHY = '#22c55e';
59
55
  const COLOR_WARNING = '#eab308';
60
56
  const COLOR_OVERLOADED = '#ef4444';
61
- const COLOR_NEUTRAL = '#94a3b8';
62
-
63
57
  /** SLO thresholds resolved for a single node (chart-level + per-node override). */
64
58
  interface NodeSlo {
65
59
  availThreshold: number | null; // fraction e.g. 0.999
@@ -106,11 +100,8 @@ interface ComputedRow {
106
100
  }
107
101
 
108
102
  // Animation constants
109
- const FLOW_DASH = '8 6'; // dash-array for animated edges
110
- const FLOW_DASH_LEN = 14; // sum of dash + gap (for offset keyframe)
111
103
  const FLOW_SPEED_MIN = 2.5; // seconds at max RPS
112
104
  const FLOW_SPEED_MAX = 6; // seconds at min RPS
113
- const OVERLOAD_PULSE_SPEED = 0.8; // seconds for overload pulse cycle
114
105
  const PARTICLE_R = 5; // particle circle radius
115
106
  const PARTICLE_COUNT_MIN = 1; // min particles per edge
116
107
  const PARTICLE_COUNT_MAX = 4; // max particles per edge (at max RPS)
@@ -742,11 +733,6 @@ function getDisplayProps(node: InfraLayoutNode, expanded: boolean, diagramOption
742
733
  }
743
734
 
744
735
 
745
- function formatRps(rps: number): string {
746
- if (rps >= 1000) return `${(rps / 1000).toFixed(1)}k rps`;
747
- return `${Math.round(rps)} rps`;
748
- }
749
-
750
736
  /** RPS value without "rps" suffix — for key-value rows where the key already says "RPS". */
751
737
  function formatRpsShort(rps: number): string {
752
738
  if (rps >= 1000) return `${(rps / 1000).toFixed(1)}k`;
@@ -5,11 +5,12 @@
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 { contrastText, mix } from '../palettes/color-utils';
9
10
  import type { PaletteColors } from '../palettes';
10
11
  import type { ParsedInitiativeStatus, InitiativeStatus } from './types';
11
12
  import type { ParticipantType } from '../sequence/parser';
12
- import type { ISLayoutResult, ISLayoutNode, ISLayoutEdge, ISLayoutGroup } from './layout';
13
+ import type { ISLayoutResult } from './layout';
13
14
  import { parseInitiativeStatus } from './parser';
14
15
  import { layoutInitiativeStatus } from './layout';
15
16
 
@@ -436,7 +437,6 @@ export function renderInitiativeStatus(
436
437
  const scale = Math.min(MAX_SCALE, scaleX, scaleY);
437
438
 
438
439
  const scaledW = diagramW * scale;
439
- const scaledH = diagramH * scale;
440
440
  const offsetX = (width - scaledW) / 2;
441
441
  const offsetY = titleHeight + DIAGRAM_PADDING;
442
442
 
@@ -850,14 +850,7 @@ export function renderInitiativeStatusForExport(
850
850
  const exportWidth = layout.width + DIAGRAM_PADDING * 2;
851
851
  const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset;
852
852
 
853
- const container = document.createElement('div');
854
- container.style.width = `${exportWidth}px`;
855
- container.style.height = `${exportHeight}px`;
856
- container.style.position = 'absolute';
857
- container.style.left = '-9999px';
858
- document.body.appendChild(container);
859
-
860
- try {
853
+ return runInExportContainer(exportWidth, exportHeight, (container) => {
861
854
  renderInitiativeStatus(
862
855
  container,
863
856
  parsed,
@@ -867,19 +860,6 @@ export function renderInitiativeStatusForExport(
867
860
  undefined,
868
861
  { width: exportWidth, height: exportHeight }
869
862
  );
870
-
871
- const svgEl = container.querySelector('svg');
872
- if (!svgEl) return '';
873
-
874
- if (theme === 'transparent') {
875
- svgEl.style.background = 'none';
876
- }
877
-
878
- svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
879
- svgEl.style.fontFamily = FONT_FAMILY;
880
-
881
- return svgEl.outerHTML;
882
- } finally {
883
- document.body.removeChild(container);
884
- }
863
+ return extractExportSvg(container, theme);
864
+ });
885
865
  }
@@ -1,5 +1,4 @@
1
1
  import type { PaletteColors } from '../palettes';
2
- import type { DgmoError } from '../diagnostics';
3
2
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
4
3
  import { resolveColor } from '../colors';
5
4
  import { matchTagBlockHeading } from '../utils/tag-groups';
@@ -15,7 +14,6 @@ import type {
15
14
  KanbanColumn,
16
15
  KanbanCard,
17
16
  KanbanTagGroup,
18
- KanbanTagEntry,
19
17
  } from './types';
20
18
 
21
19
  // ============================================================
package/src/org/layout.ts CHANGED
@@ -81,8 +81,6 @@ export interface OrgLayoutResult {
81
81
  // ============================================================
82
82
 
83
83
  const CHAR_WIDTH = 7.5;
84
- const LABEL_FONT_SIZE = 13;
85
- const META_FONT_SIZE = 11;
86
84
  const META_LINE_HEIGHT = 16;
87
85
  const HEADER_HEIGHT = 28;
88
86
  const SEPARATOR_GAP = 6;
@@ -1128,8 +1126,6 @@ export function layoutOrg(
1128
1126
  const allExpanded = expandAllLegend && activeTagGroup == null;
1129
1127
  const effectiveW = (g: OrgLegendGroup) =>
1130
1128
  activeTagGroup != null || allExpanded ? g.width : g.minifiedWidth;
1131
- const effectiveH = (g: OrgLegendGroup) =>
1132
- activeTagGroup != null || allExpanded ? g.height : g.minifiedHeight;
1133
1129
 
1134
1130
  if (visibleGroups.length > 0) {
1135
1131
  if (legendPosition === 'bottom') {
@@ -4,10 +4,11 @@
4
4
 
5
5
  import * as d3Selection from 'd3-selection';
6
6
  import { FONT_FAMILY } from '../fonts';
7
+ import { runInExportContainer, extractExportSvg } from '../utils/export-container';
7
8
  import type { PaletteColors } from '../palettes';
8
9
  import { mix } from '../palettes/color-utils';
9
10
  import type { ParsedOrg } from './parser';
10
- import type { OrgLayoutResult, OrgLayoutNode } from './layout';
11
+ import type { OrgLayoutResult } from './layout';
11
12
  import { parseOrg } from './parser';
12
13
  import { layoutOrg } from './layout';
13
14
  import {
@@ -687,38 +688,16 @@ export function renderOrgForExport(
687
688
  const layout = layoutOrg(parsed, undefined, undefined, exportHidden);
688
689
  const isDark = theme === 'dark';
689
690
 
690
- // Create offscreen container
691
- const container = document.createElement('div');
692
691
  const titleOffset = parsed.title ? TITLE_HEIGHT : 0;
693
692
  const exportWidth = layout.width + DIAGRAM_PADDING * 2;
694
- const exportHeight =
695
- layout.height + DIAGRAM_PADDING * 2 + titleOffset;
693
+ const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset;
696
694
 
697
- container.style.width = `${exportWidth}px`;
698
- container.style.height = `${exportHeight}px`;
699
- container.style.position = 'absolute';
700
- container.style.left = '-9999px';
701
- document.body.appendChild(container);
702
-
703
- try {
704
- // No hiddenAttributes passed to renderOrg — export never shows eye icons
695
+ // No hiddenAttributes passed to renderOrg — export never shows eye icons
696
+ return runInExportContainer(exportWidth, exportHeight, (container) => {
705
697
  renderOrg(container, parsed, layout, palette, isDark, undefined, {
706
698
  width: exportWidth,
707
699
  height: exportHeight,
708
700
  });
709
-
710
- const svgEl = container.querySelector('svg');
711
- if (!svgEl) return '';
712
-
713
- if (theme === 'transparent') {
714
- svgEl.style.background = 'none';
715
- }
716
-
717
- svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
718
- svgEl.style.fontFamily = FONT_FAMILY;
719
-
720
- return svgEl.outerHTML;
721
- } finally {
722
- document.body.removeChild(container);
723
- }
701
+ return extractExportSvg(container, theme);
702
+ });
724
703
  }
@@ -153,12 +153,14 @@ export interface ParsedSequenceDgmo {
153
153
  error: string | null;
154
154
  }
155
155
 
156
- // "Name is a type" pattern — e.g. "AuthService is a service"
156
+ // "Name is a type" pattern — e.g. "Auth Server is a service"
157
+ // Participant names may contain spaces; [^:]+? stops at colons so that
158
+ // note lines like "note right of A: this is a service" are not falsely matched.
157
159
  // Remainder after type is parsed separately for aka/position modifiers
158
- const IS_A_PATTERN = /^(\S+)\s+is\s+an?\s+(\w+)(?:\s+(.+))?$/i;
160
+ const IS_A_PATTERN = /^([^:]+?)\s+is\s+an?\s+(\w+)(?:\s+(.+))?$/i;
159
161
 
160
162
  // Standalone "Name position N" pattern — e.g. "DB position -1"
161
- const POSITION_ONLY_PATTERN = /^(\S+)\s+position\s+(-?\d+)$/i;
163
+ const POSITION_ONLY_PATTERN = /^([^:]+?)\s+position\s+(-?\d+)$/i;
162
164
 
163
165
  // Colored participant declaration — e.g. "Tapin2(green)", "API(blue)"
164
166
  const COLORED_PARTICIPANT_PATTERN = /^(\S+?)\(([^)]+)\)\s*$/;
@@ -174,9 +176,10 @@ const SECTION_PATTERN = /^==\s+(.+?)(?:\s*==)?\s*$/;
174
176
  // Arrow pattern for sequence inference — detects any arrow form
175
177
  const ARROW_PATTERN = /\S+\s*(?:<-\S+-|<~\S+~|-\S+->|~\S+~>|->|~>|<-|<~)\s*\S+/;
176
178
 
177
- // Note patterns — "note: text", "note right of API: text", "note left of User"
178
- const NOTE_SINGLE = /^note(?:\s+(right|left)\s+of\s+(\S+))?\s*:\s*(.+)$/i;
179
- const NOTE_MULTI = /^note(?:\s+(right|left)\s+of\s+([^\s:]+))?\s*:?\s*$/i;
179
+ // Note patterns — "note: text", "note right of Auth Server: text"
180
+ // Participant names may contain spaces; the colon acts as the delimiter.
181
+ const NOTE_SINGLE = /^note(?:\s+(right|left)\s+of\s+(.+?))?\s*:\s*(.+)$/i;
182
+ const NOTE_MULTI = /^note(?:\s+(right|left)\s+of\s+(.+?))?\s*:?\s*$/i;
180
183
 
181
184
  /**
182
185
  * Parse a .dgmo file with `chart: sequence` into a structured representation.
@@ -673,7 +676,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
673
676
 
674
677
  // ---- Error: plain bidirectional arrows (A <-> B, A <~> B) ----
675
678
  const bidiPlainMatch = arrowCore.match(
676
- /^(\S+)\s*(?:<->|<~>)\s*(\S+)/
679
+ /^(.+?)\s*(?:<->|<~>)\s*(.+)/
677
680
  );
678
681
  if (bidiPlainMatch) {
679
682
  pushError(
@@ -684,8 +687,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
684
687
  }
685
688
 
686
689
  // ---- Deprecated bare return arrows: A <- B, A <~ B ----
687
- const bareReturnSync = arrowCore.match(/^(\S+)\s+<-\s+(\S+)$/);
688
- const bareReturnAsync = arrowCore.match(/^(\S+)\s+<~\s+(\S+)$/);
690
+ const bareReturnSync = arrowCore.match(/^(.+?)\s+<-\s+(.+)$/);
691
+ const bareReturnAsync = arrowCore.match(/^(.+?)\s+<~\s+(.+)$/);
689
692
  const bareReturn = bareReturnSync || bareReturnAsync;
690
693
  if (bareReturn) {
691
694
  const to = bareReturn[1];
@@ -698,8 +701,8 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
698
701
  }
699
702
 
700
703
  // ---- Bare (unlabeled) call arrows: A -> B, A ~> B ----
701
- const bareCallSync = arrowCore.match(/^(\S+)\s*->\s*(\S+)$/);
702
- const bareCallAsync = arrowCore.match(/^(\S+)\s*~>\s*(\S+)$/);
704
+ const bareCallSync = arrowCore.match(/^(.+?)\s*->\s*(.+)$/);
705
+ const bareCallAsync = arrowCore.match(/^(.+?)\s*~>\s*(.+)$/);
703
706
  const bareCall = bareCallSync || bareCallAsync;
704
707
  if (bareCall) {
705
708
  contentStarted = true;
@@ -10,7 +10,6 @@ import {
10
10
  truncateBareUrl,
11
11
  renderInlineText,
12
12
  } from '../utils/inline-markdown';
13
- export type { InlineSpan } from '../utils/inline-markdown';
14
13
  export { parseInlineMarkdown, truncateBareUrl };
15
14
  import { FONT_FAMILY } from '../fonts';
16
15
  import { resolveColor } from '../colors';
@@ -2129,7 +2128,6 @@ export function renderSequenceDiagram(
2129
2128
  // IMPORTANT: only the <g> carries data-line-number / data-section —
2130
2129
  // children must NOT have them, otherwise the click walk-up resolves
2131
2130
  // to a line-number navigation before reaching data-section-toggle.
2132
- const HIT_AREA_HEIGHT = 36;
2133
2131
  const sectionG = svg
2134
2132
  .append('g')
2135
2133
  .attr('data-section-toggle', '')
@@ -5,7 +5,6 @@
5
5
  // Resolves effective tag values for participants and messages
6
6
  // using the priority chain: explicit > group > receiver-inherit > default > neutral
7
7
 
8
- import type { TagGroup } from '../utils/tag-groups';
9
8
  import type {
10
9
  ParsedSequenceDgmo,
11
10
  SequenceParticipant,
@@ -3,7 +3,7 @@
3
3
  // ============================================================
4
4
 
5
5
  import dagre from '@dagrejs/dagre';
6
- import type { ParsedSitemap, SitemapNode, SitemapEdge } from './types';
6
+ import type { ParsedSitemap, SitemapNode } from './types';
7
7
  import type { TagGroup } from '../utils/tag-groups';
8
8
  import { resolveTagColor, injectDefaultTagMetadata } from '../utils/tag-groups';
9
9
 
@@ -89,8 +89,6 @@ export interface SitemapLayoutResult {
89
89
  // ============================================================
90
90
 
91
91
  const CHAR_WIDTH = 7.5;
92
- const LABEL_FONT_SIZE = 13;
93
- const META_FONT_SIZE = 11;
94
92
  const META_LINE_HEIGHT = 16;
95
93
  const HEADER_HEIGHT = 28;
96
94
  const SEPARATOR_GAP = 6;
@@ -105,7 +103,6 @@ const CONTAINER_LABEL_HEIGHT = 28;
105
103
  const CONTAINER_META_LINE_HEIGHT = 16;
106
104
 
107
105
  // Legend (kanban-style pills)
108
- const LEGEND_GAP = 30;
109
106
  const LEGEND_HEIGHT = 28;
110
107
  const LEGEND_PILL_PAD = 16;
111
108
  const LEGEND_PILL_FONT_W = 11 * 0.6;
@@ -162,16 +159,6 @@ function resolveNodeColor(
162
159
 
163
160
  const OVERLAP_GAP = 20;
164
161
 
165
- function countDescendantNodes(node: SitemapNode, hiddenCounts?: Map<string, number>): number {
166
- let count = 0;
167
- for (const child of node.children) {
168
- count += (child.isContainer ? 0 : 1) + countDescendantNodes(child, hiddenCounts);
169
- const hc = hiddenCounts?.get(child.id);
170
- if (hc) count += hc;
171
- }
172
- return count;
173
- }
174
-
175
162
  // ============================================================
176
163
  // Legend
177
164
  // ============================================================
@@ -5,7 +5,7 @@
5
5
  import type { PaletteColors } from '../palettes';
6
6
  import { resolveColor } from '../colors';
7
7
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
8
- import type { TagGroup, TagEntry } from '../utils/tag-groups';
8
+ import type { TagGroup } from '../utils/tag-groups';
9
9
  import { isTagBlockHeading, matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
10
10
  import {
11
11
  measureIndent,
@@ -17,7 +17,6 @@ import {
17
17
  } from '../utils/parsing';
18
18
  import type {
19
19
  SitemapNode,
20
- SitemapEdge,
21
20
  SitemapDirection,
22
21
  ParsedSitemap,
23
22
  } from './types';
@@ -10,9 +10,6 @@ import { mix } from '../palettes/color-utils';
10
10
  import type { ParsedSitemap } from './types';
11
11
  import type {
12
12
  SitemapLayoutResult,
13
- SitemapLayoutNode,
14
- SitemapLayoutEdge,
15
- SitemapContainerBounds,
16
13
  SitemapLegendGroup,
17
14
  } from './layout';
18
15
  import {
@@ -13,15 +13,15 @@ export interface ParsedArrow {
13
13
  async: boolean;
14
14
  }
15
15
 
16
- // Forward (call) patterns
17
- const SYNC_LABELED_RE = /^(\S+)\s+-(.+)->\s+(\S+)$/;
18
- const ASYNC_LABELED_RE = /^(\S+)\s+~(.+)~>\s+(\S+)$/;
16
+ // Forward (call) patterns — participant names may contain spaces, so use non-greedy (.+?)
17
+ const SYNC_LABELED_RE = /^(.+?)\s+-(.+)->\s+(.+)$/;
18
+ const ASYNC_LABELED_RE = /^(.+?)\s+~(.+)~>\s+(.+)$/;
19
19
 
20
20
  // Deprecated patterns — produce errors
21
- const RETURN_SYNC_LABELED_RE = /^(\S+)\s+<-(.+)-\s+(\S+)$/;
22
- const RETURN_ASYNC_LABELED_RE = /^(\S+)\s+<~(.+)~\s+(\S+)$/;
23
- const BIDI_SYNC_RE = /^(\S+)\s+<-(.+)->\s+(\S+)$/;
24
- const BIDI_ASYNC_RE = /^(\S+)\s+<~(.+)~>\s+(\S+)$/;
21
+ const RETURN_SYNC_LABELED_RE = /^(.+?)\s+<-(.+)-\s+(.+)$/;
22
+ const RETURN_ASYNC_LABELED_RE = /^(.+?)\s+<~(.+)~\s+(.+)$/;
23
+ const BIDI_SYNC_RE = /^(.+?)\s+<-(.+)->\s+(.+)$/;
24
+ const BIDI_ASYNC_RE = /^(.+?)\s+<~(.+)~>\s+(.+)$/;
25
25
 
26
26
  const ARROW_CHARS = ['->', '~>'];
27
27
 
@@ -0,0 +1,40 @@
1
+ import { FONT_FAMILY } from '../fonts';
2
+
3
+ /**
4
+ * Creates an offscreen DOM container at the given dimensions, runs `fn` inside it,
5
+ * then removes it (try/finally). Returns whatever `fn` returns.
6
+ */
7
+ export function runInExportContainer<T>(
8
+ width: number,
9
+ height: number,
10
+ fn: (container: HTMLDivElement) => T,
11
+ ): T {
12
+ const container = document.createElement('div');
13
+ container.style.width = `${width}px`;
14
+ container.style.height = `${height}px`;
15
+ container.style.position = 'absolute';
16
+ container.style.left = '-9999px';
17
+ document.body.appendChild(container);
18
+ try {
19
+ return fn(container);
20
+ } finally {
21
+ document.body.removeChild(container);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Extracts the SVG element from an export container, applies required export attributes
27
+ * (xmlns, fontFamily, transparent background if requested), and returns its outerHTML.
28
+ * Returns '' if no SVG element is found.
29
+ */
30
+ export function extractExportSvg(
31
+ container: HTMLElement,
32
+ theme: 'light' | 'dark' | 'transparent',
33
+ ): string {
34
+ const svgEl = container.querySelector('svg');
35
+ if (!svgEl) return '';
36
+ if (theme === 'transparent') svgEl.style.background = 'none';
37
+ svgEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
38
+ svgEl.style.fontFamily = FONT_FAMILY;
39
+ return svgEl.outerHTML;
40
+ }