@diagrammo/dgmo 0.8.19 → 0.8.21

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 (74) hide show
  1. package/dist/cli.cjs +92 -131
  2. package/dist/editor.cjs +13 -1
  3. package/dist/editor.cjs.map +1 -1
  4. package/dist/editor.js +13 -1
  5. package/dist/editor.js.map +1 -1
  6. package/dist/highlight.cjs +13 -1
  7. package/dist/highlight.cjs.map +1 -1
  8. package/dist/highlight.js +13 -1
  9. package/dist/highlight.js.map +1 -1
  10. package/dist/index.cjs +4524 -1511
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +427 -186
  13. package/dist/index.d.ts +427 -186
  14. package/dist/index.js +4526 -1503
  15. package/dist/index.js.map +1 -1
  16. package/docs/guide/chart-mindmap.md +198 -0
  17. package/docs/guide/chart-sequence.md +23 -1
  18. package/docs/guide/chart-wireframe.md +100 -0
  19. package/docs/guide/index.md +8 -0
  20. package/docs/language-reference.md +210 -2
  21. package/package.json +22 -9
  22. package/src/boxes-and-lines/collapse.ts +21 -3
  23. package/src/boxes-and-lines/layout.ts +51 -9
  24. package/src/boxes-and-lines/parser.ts +16 -4
  25. package/src/boxes-and-lines/renderer.ts +121 -23
  26. package/src/boxes-and-lines/types.ts +1 -0
  27. package/src/c4/parser.ts +8 -7
  28. package/src/class/parser.ts +6 -0
  29. package/src/cli.ts +1 -9
  30. package/src/completion.ts +26 -0
  31. package/src/d3.ts +169 -266
  32. package/src/dgmo-router.ts +103 -5
  33. package/src/diagnostics.ts +16 -6
  34. package/src/echarts.ts +43 -10
  35. package/src/editor/keywords.ts +12 -0
  36. package/src/er/parser.ts +22 -2
  37. package/src/gantt/renderer.ts +2 -2
  38. package/src/graph/flowchart-parser.ts +89 -52
  39. package/src/graph/layout.ts +73 -9
  40. package/src/graph/state-collapse.ts +78 -0
  41. package/src/graph/state-parser.ts +60 -35
  42. package/src/graph/state-renderer.ts +139 -34
  43. package/src/index.ts +41 -16
  44. package/src/infra/parser.ts +9 -2
  45. package/src/kanban/renderer.ts +305 -59
  46. package/src/mindmap/collapse.ts +88 -0
  47. package/src/mindmap/layout.ts +605 -0
  48. package/src/mindmap/parser.ts +379 -0
  49. package/src/mindmap/renderer.ts +543 -0
  50. package/src/mindmap/text-wrap.ts +207 -0
  51. package/src/mindmap/types.ts +55 -0
  52. package/src/palettes/color-utils.ts +4 -12
  53. package/src/palettes/index.ts +0 -4
  54. package/src/render.ts +31 -20
  55. package/src/sequence/parser.ts +7 -2
  56. package/src/sequence/renderer.ts +141 -21
  57. package/src/sharing.ts +2 -0
  58. package/src/sitemap/layout.ts +35 -12
  59. package/src/sitemap/renderer.ts +1 -6
  60. package/src/utils/arrows.ts +180 -11
  61. package/src/utils/d3-types.ts +4 -0
  62. package/src/utils/export-container.ts +3 -2
  63. package/src/utils/legend-constants.ts +0 -4
  64. package/src/utils/legend-d3.ts +1 -0
  65. package/src/utils/legend-layout.ts +2 -2
  66. package/src/utils/parsing.ts +2 -0
  67. package/src/utils/time-ticks.ts +213 -0
  68. package/src/wireframe/layout.ts +460 -0
  69. package/src/wireframe/parser.ts +956 -0
  70. package/src/wireframe/renderer.ts +1293 -0
  71. package/src/wireframe/types.ts +110 -0
  72. package/src/branding.ts +0 -67
  73. package/src/dgmo-mermaid.ts +0 -262
  74. package/src/palettes/mermaid-bridge.ts +0 -220
@@ -0,0 +1,543 @@
1
+ // ============================================================
2
+ // Mindmap SVG Renderer
3
+ // ============================================================
4
+
5
+ import * as d3Selection from 'd3-selection';
6
+ import { FONT_FAMILY } from '../fonts';
7
+ import {
8
+ runInExportContainer,
9
+ extractExportSvg,
10
+ } from '../utils/export-container';
11
+ import type { PaletteColors } from '../palettes';
12
+ import { mix } from '../palettes/color-utils';
13
+ import type { ParsedMindmap } from './types';
14
+ import type { MindmapLayoutResult } from './types';
15
+ import { parseMindmap } from './parser';
16
+ import { layoutMindmap } from './layout';
17
+ import { computeNodeText } from './text-wrap';
18
+ import { renderLegendD3 } from '../utils/legend-d3';
19
+ import type { LegendConfig, LegendState } from '../utils/legend-types';
20
+ import { LEGEND_HEIGHT, LEGEND_GROUP_GAP } from '../utils/legend-constants';
21
+ import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
22
+
23
+ // ============================================================
24
+ // Constants
25
+ // ============================================================
26
+
27
+ const DIAGRAM_PADDING = 20;
28
+ const MAX_SCALE = 3;
29
+ const TITLE_HEIGHT = 30;
30
+ const SINGLE_LABEL_HEIGHT = 28;
31
+ const LABEL_LINE_HEIGHT = 18;
32
+ const DESC_LINE_HEIGHT = 14;
33
+ const NODE_RADIUS = 6;
34
+ const ROOT_STROKE_WIDTH = 2.5;
35
+ const NODE_STROKE_WIDTH = 1.5;
36
+ const EDGE_STROKE_WIDTH = 1.5;
37
+ const COLLAPSE_BAR_HEIGHT = 6;
38
+
39
+ function nodeFill(
40
+ palette: PaletteColors,
41
+ isDark: boolean,
42
+ nodeColor?: string
43
+ ): string {
44
+ const color = nodeColor ?? palette.primary;
45
+ return mix(color, isDark ? palette.surface : palette.bg, 25);
46
+ }
47
+
48
+ function nodeStroke(palette: PaletteColors, nodeColor?: string): string {
49
+ return nodeColor ?? palette.primary;
50
+ }
51
+
52
+ /** Depth color sequence — ROYGBIV-ish from the palette's named colors */
53
+ const DEPTH_COLOR_KEYS = [
54
+ 'red',
55
+ 'orange',
56
+ 'yellow',
57
+ 'green',
58
+ 'blue',
59
+ 'purple',
60
+ 'teal',
61
+ 'cyan',
62
+ ];
63
+
64
+ function depthColor(depth: number, palette: PaletteColors): string {
65
+ const key = DEPTH_COLOR_KEYS[depth] as
66
+ | keyof typeof palette.colors
67
+ | undefined;
68
+ if (key && key in palette.colors) {
69
+ return palette.colors[key];
70
+ }
71
+ return palette.colors.gray;
72
+ }
73
+
74
+ // ============================================================
75
+ // Main renderer
76
+ // ============================================================
77
+
78
+ export function renderMindmap(
79
+ container: HTMLDivElement,
80
+ parsed: ParsedMindmap,
81
+ layout: MindmapLayoutResult,
82
+ palette: PaletteColors,
83
+ isDark: boolean,
84
+ onClickItem?: (lineNumber: number) => void,
85
+ exportDims?: { width?: number; height?: number },
86
+ onToggleNode?: (nodeId: string) => void,
87
+ hideDescriptions?: boolean,
88
+ activeTagGroup?: string | null,
89
+ options?: {
90
+ colorByDepth?: boolean;
91
+ onToggleColorByDepth?: (active: boolean) => void;
92
+ onToggleDescriptions?: (active: boolean) => void;
93
+ controlsExpanded?: boolean;
94
+ onToggleControlsExpand?: () => void;
95
+ }
96
+ ): void {
97
+ const isExport = !!exportDims;
98
+ const containerWidth =
99
+ exportDims?.width ?? (container.getBoundingClientRect().width || 800);
100
+ const containerHeight =
101
+ exportDims?.height ?? (container.getBoundingClientRect().height || 600);
102
+
103
+ // Clear existing content
104
+ d3Selection.select(container).selectAll('*').remove();
105
+
106
+ const svg = d3Selection
107
+ .select(container)
108
+ .append('svg')
109
+ .attr('width', containerWidth)
110
+ .attr('height', containerHeight)
111
+ .style('font-family', FONT_FAMILY);
112
+
113
+ // Reserve space for fixed elements (legend, title) in interactive mode
114
+ const hasControls =
115
+ !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
116
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls;
117
+ const fixedLegend = !isExport && hasLegend;
118
+ const legendReserve = fixedLegend ? LEGEND_HEIGHT + LEGEND_GROUP_GAP : 0;
119
+ const fixedTitle = !isExport && !!parsed.title;
120
+ const titleReserve = fixedTitle ? TITLE_HEIGHT : 0;
121
+
122
+ // Compute scale to fit diagram in available space
123
+ const availWidth = containerWidth;
124
+ const availHeight =
125
+ containerHeight - DIAGRAM_PADDING * 2 - legendReserve - titleReserve;
126
+
127
+ let scale: number;
128
+ if (isExport) {
129
+ scale = 1;
130
+ } else {
131
+ const scaleX = layout.width > 0 ? availWidth / layout.width : 1;
132
+ const scaleY = layout.height > 0 ? availHeight / layout.height : 1;
133
+ scale = Math.min(scaleX, scaleY, MAX_SCALE);
134
+ }
135
+
136
+ const scaledWidth = layout.width * scale;
137
+ const scaledHeight = layout.height * scale;
138
+ const offsetX = (availWidth - scaledWidth) / 2;
139
+ const offsetY =
140
+ DIAGRAM_PADDING +
141
+ legendReserve +
142
+ titleReserve +
143
+ (availHeight - scaledHeight) / 2;
144
+
145
+ // Main group with scale transform (created early so title can reference it in export mode)
146
+ const mainG = svg
147
+ .append('g')
148
+ .attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
149
+
150
+ // Title — fixed at top in app mode (above legend), inside scaled group in export
151
+ if (parsed.title) {
152
+ const titleParent = fixedTitle ? svg : mainG;
153
+ const titleX = fixedTitle ? containerWidth / 2 : layout.width / 2;
154
+ const titleY = fixedTitle
155
+ ? DIAGRAM_PADDING + TITLE_FONT_SIZE
156
+ : TITLE_FONT_SIZE;
157
+ const titleText = titleParent
158
+ .append('text')
159
+ .attr('x', titleX)
160
+ .attr('y', titleY)
161
+ .attr('text-anchor', 'middle')
162
+ .attr('font-size', TITLE_FONT_SIZE)
163
+ .attr('font-weight', TITLE_FONT_WEIGHT)
164
+ .attr('fill', palette.text)
165
+ .attr('class', 'chart-title')
166
+ .text(parsed.title);
167
+
168
+ if (parsed.titleLineNumber) {
169
+ titleText.attr('data-line-number', parsed.titleLineNumber);
170
+ }
171
+ if (onClickItem && parsed.titleLineNumber) {
172
+ titleText
173
+ .style('cursor', 'pointer')
174
+ .on('click', () => onClickItem(parsed.titleLineNumber!));
175
+ }
176
+ }
177
+
178
+ // Legend — fixed below title, outside scaled group, in interactive mode
179
+ if (fixedLegend) {
180
+ const legendG = svg
181
+ .append('g')
182
+ .attr('class', 'mindmap-legend-fixed')
183
+ .attr('transform', `translate(0, ${DIAGRAM_PADDING + titleReserve})`);
184
+
185
+ // Collect used tag values from all nodes to filter legend entries
186
+ const usedValues = new Map<string, Set<string>>(); // groupName → set of used values
187
+ for (const node of layout.nodes) {
188
+ for (const tg of parsed.tagGroups) {
189
+ const key = tg.name.toLowerCase();
190
+ const val = node.metadata[key];
191
+ if (val) {
192
+ if (!usedValues.has(key)) usedValues.set(key, new Set());
193
+ usedValues.get(key)!.add(val.toLowerCase());
194
+ }
195
+ }
196
+ }
197
+
198
+ // Build controls toggles
199
+ const toggles: import('../utils/legend-types').ControlsGroupToggle[] = [];
200
+ if (options?.onToggleDescriptions) {
201
+ toggles.push({
202
+ id: 'descriptions',
203
+ type: 'toggle' as const,
204
+ label: 'Descriptions',
205
+ active: !hideDescriptions,
206
+ onToggle: (active) => options.onToggleDescriptions!(active),
207
+ });
208
+ }
209
+ if (options?.onToggleColorByDepth) {
210
+ toggles.push({
211
+ id: 'depth-colors',
212
+ type: 'toggle' as const,
213
+ label: 'Depth Colors',
214
+ active: options.colorByDepth ?? false,
215
+ onToggle: options.onToggleColorByDepth,
216
+ });
217
+ }
218
+ const controlsToggles: LegendConfig['controlsGroup'] =
219
+ toggles.length > 0 ? { toggles } : undefined;
220
+
221
+ const legendConfig: LegendConfig = {
222
+ groups: parsed.tagGroups.map((tg) => {
223
+ const used = usedValues.get(tg.name.toLowerCase());
224
+ return {
225
+ name: tg.name,
226
+ alias: tg.alias,
227
+ entries: tg.entries
228
+ .filter((e) => used?.has(e.value.toLowerCase()))
229
+ .map((e) => ({ value: e.value, color: e.color })),
230
+ };
231
+ }),
232
+ position: { placement: 'top-center', titleRelation: 'below-title' },
233
+ mode: 'fixed',
234
+ controlsGroup: controlsToggles,
235
+ };
236
+ const legendState: LegendState = {
237
+ activeGroup: options?.colorByDepth
238
+ ? null
239
+ : activeTagGroup !== undefined
240
+ ? activeTagGroup
241
+ : (parsed.options['active-tag'] ?? null),
242
+ hiddenAttributes: new Set(),
243
+ controlsExpanded: options?.controlsExpanded,
244
+ };
245
+ const legendPalette = {
246
+ text: palette.text,
247
+ textMuted: palette.textMuted,
248
+ bg: palette.bg,
249
+ surface: palette.surface,
250
+ primary: palette.primary,
251
+ };
252
+ const legendCallbacks: import('../utils/legend-types').LegendCallbacks = {
253
+ onControlsExpand: options?.onToggleControlsExpand,
254
+ onControlsToggle: (id, active) => {
255
+ if (id === 'depth-colors' && options?.onToggleColorByDepth) {
256
+ options.onToggleColorByDepth(active);
257
+ }
258
+ if (id === 'descriptions' && options?.onToggleDescriptions) {
259
+ options.onToggleDescriptions(active);
260
+ }
261
+ },
262
+ };
263
+ renderLegendD3(
264
+ legendG,
265
+ legendConfig,
266
+ legendState,
267
+ legendPalette,
268
+ isDark,
269
+ legendCallbacks,
270
+ containerWidth
271
+ );
272
+ }
273
+
274
+ // Render edges (background layer)
275
+ for (const edge of layout.edges) {
276
+ mainG
277
+ .append('path')
278
+ .attr('class', 'mindmap-edge')
279
+ .attr('d', edge.path)
280
+ .attr('fill', 'none')
281
+ .attr('stroke', palette.textMuted)
282
+ .attr('stroke-width', EDGE_STROKE_WIDTH)
283
+ .attr('stroke-opacity', 0.5);
284
+ }
285
+
286
+ // Render nodes (foreground layer)
287
+ for (const node of layout.nodes) {
288
+ const isRoot = node.radius === 0 && layout.nodes.indexOf(node) === 0;
289
+ const strokeW = isRoot ? ROOT_STROKE_WIDTH : NODE_STROKE_WIDTH;
290
+ const effectiveColor = options?.colorByDepth
291
+ ? depthColor(node.depth, palette)
292
+ : node.color;
293
+ const fill = nodeFill(palette, isDark, effectiveColor);
294
+ const stroke = nodeStroke(palette, effectiveColor);
295
+
296
+ const nodeG = mainG
297
+ .append('g')
298
+ .attr('class', 'mindmap-node')
299
+ .attr('data-line-number', node.lineNumber);
300
+
301
+ // Expose active tag group value for legend-entry hover dimming
302
+ if (activeTagGroup) {
303
+ const tagKey = activeTagGroup.toLowerCase();
304
+ const metaValue = node.metadata[tagKey];
305
+ if (metaValue) {
306
+ nodeG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
307
+ }
308
+ }
309
+
310
+ // Add collapse toggle attributes for nodes with children
311
+ if (node.hasChildren) {
312
+ nodeG
313
+ .attr('data-node-toggle', node.id)
314
+ .attr('tabindex', '0')
315
+ .attr('role', 'button')
316
+ .attr(
317
+ 'aria-expanded',
318
+ node.hiddenCount != null && node.hiddenCount > 0 ? 'false' : 'true'
319
+ )
320
+ .attr('aria-label', node.label);
321
+ }
322
+
323
+ // Node rectangle
324
+ nodeG
325
+ .append('rect')
326
+ .attr('x', node.x)
327
+ .attr('y', node.y)
328
+ .attr('width', node.width)
329
+ .attr('height', node.height)
330
+ .attr('rx', NODE_RADIUS)
331
+ .attr('ry', NODE_RADIUS)
332
+ .attr('fill', fill)
333
+ .attr('stroke', stroke)
334
+ .attr('stroke-width', strokeW);
335
+
336
+ // Determine if description is visible (needed for label centering)
337
+ const collapsed = node.hiddenCount != null && node.hiddenCount > 0;
338
+ const showDesc = !hideDescriptions && !!node.description && !collapsed;
339
+
340
+ // Compute wrapped text layout (same logic as layout.ts for sizing agreement)
341
+ const textLayout = computeNodeText(
342
+ node.label,
343
+ node.description,
344
+ node.depth,
345
+ node.width,
346
+ hideDescriptions || collapsed
347
+ );
348
+ const {
349
+ labelLines,
350
+ labelFontSize: fontSize,
351
+ descLines,
352
+ descFontSize,
353
+ } = textLayout;
354
+
355
+ // Label zone height
356
+ const labelLineCount = labelLines.length;
357
+ const labelZoneH =
358
+ labelLineCount <= 1
359
+ ? SINGLE_LABEL_HEIGHT
360
+ : LABEL_LINE_HEIGHT * labelLineCount;
361
+ const labelZoneHeight = showDesc ? labelZoneH : node.height;
362
+
363
+ // Label text — vertically centered in the label zone
364
+ const centerX = node.x + node.width / 2;
365
+ if (labelLineCount <= 1) {
366
+ // Single line — simple centering
367
+ const labelY = node.y + labelZoneHeight / 2 + fontSize * 0.35;
368
+ nodeG
369
+ .append('text')
370
+ .attr('x', centerX)
371
+ .attr('y', labelY)
372
+ .attr('text-anchor', 'middle')
373
+ .attr('font-size', fontSize)
374
+ .attr('font-weight', isRoot ? 'bold' : 'normal')
375
+ .attr('fill', palette.text)
376
+ .text(labelLines[0]);
377
+ } else {
378
+ // Multi-line — use tspan elements
379
+ // Visual text block spans from first baseline to last baseline:
380
+ // blockH = (lineCount - 1) * lineHeight
381
+ // Center that block in the zone, then offset each baseline by fontSize * 0.35
382
+ const blockH = LABEL_LINE_HEIGHT * (labelLineCount - 1);
383
+ const firstBaselineY =
384
+ node.y + labelZoneHeight / 2 - blockH / 2 + fontSize * 0.35;
385
+ const textEl = nodeG
386
+ .append('text')
387
+ .attr('x', centerX)
388
+ .attr('text-anchor', 'middle')
389
+ .attr('font-size', fontSize)
390
+ .attr('font-weight', isRoot ? 'bold' : 'normal')
391
+ .attr('fill', palette.text);
392
+
393
+ for (let i = 0; i < labelLines.length; i++) {
394
+ textEl
395
+ .append('tspan')
396
+ .attr('x', centerX)
397
+ .attr('y', firstBaselineY + i * LABEL_LINE_HEIGHT)
398
+ .text(labelLines[i]);
399
+ }
400
+ }
401
+
402
+ // Hover tooltip for truncated/wrapped labels — on the <g>, not on <text>
403
+ if (labelLines.length > 1 || labelLines[0] !== node.label) {
404
+ nodeG.append('title').text(node.label);
405
+ }
406
+
407
+ // Description — separator line + muted text below label
408
+ if (showDesc && descLines.length > 0) {
409
+ const separatorY = node.y + labelZoneH;
410
+
411
+ // Separator line
412
+ nodeG
413
+ .append('line')
414
+ .attr('x1', node.x)
415
+ .attr('y1', separatorY)
416
+ .attr('x2', node.x + node.width)
417
+ .attr('y2', separatorY)
418
+ .attr('stroke', stroke)
419
+ .attr('stroke-opacity', 0.3)
420
+ .attr('stroke-width', 1);
421
+
422
+ // Description text
423
+ if (descLines.length <= 1) {
424
+ const descY = separatorY + 4 + descFontSize;
425
+ nodeG
426
+ .append('text')
427
+ .attr('x', centerX)
428
+ .attr('y', descY)
429
+ .attr('text-anchor', 'middle')
430
+ .attr('font-size', descFontSize)
431
+ .attr('fill', palette.textMuted)
432
+ .text(descLines[0]);
433
+ } else {
434
+ const descStartY = separatorY + 4 + descFontSize;
435
+ const descTextEl = nodeG
436
+ .append('text')
437
+ .attr('x', centerX)
438
+ .attr('text-anchor', 'middle')
439
+ .attr('font-size', descFontSize)
440
+ .attr('fill', palette.textMuted);
441
+ for (let i = 0; i < descLines.length; i++) {
442
+ descTextEl
443
+ .append('tspan')
444
+ .attr('x', centerX)
445
+ .attr('y', descStartY + i * DESC_LINE_HEIGHT)
446
+ .text(descLines[i]);
447
+ }
448
+ }
449
+ }
450
+
451
+ // Collapse drill-bar (interactive mode only)
452
+ if (!isExport && node.hiddenCount != null && node.hiddenCount > 0) {
453
+ // Clip path for rounded bottom
454
+ const clipId = `collapse-clip-${node.id}`;
455
+ const defs = mainG.append('defs');
456
+ defs
457
+ .append('clipPath')
458
+ .attr('id', clipId)
459
+ .append('rect')
460
+ .attr('x', node.x)
461
+ .attr('y', node.y)
462
+ .attr('width', node.width)
463
+ .attr('height', node.height)
464
+ .attr('rx', NODE_RADIUS)
465
+ .attr('ry', NODE_RADIUS);
466
+
467
+ nodeG
468
+ .append('rect')
469
+ .attr('class', 'collapse-bar')
470
+ .attr('x', node.x)
471
+ .attr('y', node.y + node.height - COLLAPSE_BAR_HEIGHT)
472
+ .attr('width', node.width)
473
+ .attr('height', COLLAPSE_BAR_HEIGHT)
474
+ .attr('fill', stroke)
475
+ .attr('clip-path', `url(#${clipId})`);
476
+ }
477
+
478
+ // Click handler
479
+ if (onClickItem) {
480
+ nodeG.style('cursor', 'pointer').on('click', (event: Event) => {
481
+ // If this node has a toggle and the toggle callback exists,
482
+ // use toggle behavior instead of navigation
483
+ if (node.hasChildren && onToggleNode) {
484
+ event.stopPropagation();
485
+ onToggleNode(node.id);
486
+ } else {
487
+ onClickItem(node.lineNumber);
488
+ }
489
+ });
490
+ }
491
+
492
+ // Hover opacity
493
+ if (!isExport) {
494
+ nodeG
495
+ .on('mouseenter', function () {
496
+ d3Selection.select(this).attr('opacity', 0.7);
497
+ })
498
+ .on('mouseleave', function () {
499
+ d3Selection.select(this).attr('opacity', 1);
500
+ });
501
+ }
502
+ }
503
+ }
504
+
505
+ // ============================================================
506
+ // Export convenience function
507
+ // ============================================================
508
+
509
+ export function renderMindmapForExport(
510
+ content: string,
511
+ theme: 'light' | 'dark' | 'transparent',
512
+ palette: PaletteColors
513
+ ): string {
514
+ const parsed = parseMindmap(content, palette);
515
+ if (parsed.error) return '';
516
+
517
+ const isDark = theme === 'dark';
518
+ const hideDescriptions = parsed.options['hide-descriptions'] === 'true';
519
+
520
+ const layout = layoutMindmap(parsed, palette, {
521
+ interactive: false,
522
+ hideDescriptions,
523
+ });
524
+
525
+ const titleOffset = parsed.title ? TITLE_HEIGHT : 0;
526
+ const exportWidth = layout.width + DIAGRAM_PADDING * 2;
527
+ const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset;
528
+
529
+ return runInExportContainer(exportWidth, exportHeight, (container) => {
530
+ renderMindmap(
531
+ container,
532
+ parsed,
533
+ layout,
534
+ palette,
535
+ isDark,
536
+ undefined,
537
+ { width: exportWidth, height: exportHeight },
538
+ undefined,
539
+ hideDescriptions
540
+ );
541
+ return extractExportSvg(container, theme);
542
+ });
543
+ }