zombie-mermaid 1.3.0 → 1.5.0

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 (35) hide show
  1. package/README.md +6 -10
  2. package/dist/cli.js +344 -45
  3. package/dist/index.cjs +540 -241
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +90 -1
  6. package/dist/index.d.ts +90 -1
  7. package/dist/index.js +540 -241
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -1
  10. package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
  11. package/src/__tests__/ascii-display-width-unit.test.ts +129 -0
  12. package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +212 -0
  13. package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
  14. package/src/__tests__/edge-cell-styles.test.ts +177 -0
  15. package/src/__tests__/grid-occupancy.test.ts +160 -0
  16. package/src/__tests__/helpers/terminal-display-width.ts +41 -13
  17. package/src/__tests__/integration.test.ts +107 -0
  18. package/src/__tests__/interactivity-config.test.ts +64 -0
  19. package/src/__tests__/interactivity-option.test.ts +141 -0
  20. package/src/__tests__/sequence-integration.test.ts +28 -0
  21. package/src/__tests__/svg-accessible-name.test.ts +392 -0
  22. package/src/__tests__/theme.test.ts +24 -0
  23. package/src/ascii/display-width.ts +92 -26
  24. package/src/ascii/edge-cell-styles.ts +81 -0
  25. package/src/ascii/grid-occupancy.ts +98 -0
  26. package/src/ascii/grid.ts +102 -3
  27. package/src/class/renderer.ts +23 -1
  28. package/src/er/renderer.ts +23 -1
  29. package/src/index.ts +99 -4
  30. package/src/parser.ts +52 -16
  31. package/src/renderer.ts +95 -11
  32. package/src/sequence/renderer.ts +23 -1
  33. package/src/theme.ts +153 -2
  34. package/src/types.ts +76 -1
  35. package/src/xychart/renderer.ts +22 -6
package/README.md CHANGED
@@ -6,11 +6,14 @@
6
6
 
7
7
  Ultra-fast, fully themeable, zero DOM dependencies. A maintained fork of [`beautiful-mermaid`](https://github.com/lukilabs/beautiful-mermaid).
8
8
 
9
- ![zombie-mermaid sequence diagram example](hero.png)
9
+ ![Raw Mermaid source rendering into a themed, animated diagram, drawn by zombie-mermaid itself](hero.svg)
10
10
 
11
- [**Live demo →**](https://dfadler.github.io/zombie-mermaid/)
11
+ [![Live Demo](https://img.shields.io/badge/Live_Demo-view_the_sample_gallery-1f6feb?style=for-the-badge)](https://dfadler.github.io/zombie-mermaid/)
12
12
 
13
13
  [![npm version](https://img.shields.io/npm/v/zombie-mermaid.svg)](https://www.npmjs.com/package/zombie-mermaid)
14
+ [![CI](https://github.com/dfadler/zombie-mermaid/actions/workflows/ci.yml/badge.svg)](https://github.com/dfadler/zombie-mermaid/actions/workflows/ci.yml)
15
+ [![codecov](https://codecov.io/gh/dfadler/zombie-mermaid/branch/main/graph/badge.svg)](https://codecov.io/gh/dfadler/zombie-mermaid)
16
+ [![Socket Security](https://socket.dev/api/badge/npm/package/zombie-mermaid)](https://socket.dev/npm/package/zombie-mermaid)
14
17
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
15
18
 
16
19
  </div>
@@ -122,6 +125,7 @@ zombie-mermaid --help # Show all options
122
125
  - [Supported Diagrams](docs/diagrams.md) — syntax for every diagram type, XY chart styling, and ASCII rendering options
123
126
  - [React Integration](docs/react-integration.md) — zero-flash rendering with `useMemo()`
124
127
  - [API Reference](docs/api-reference.md) — full function and options reference
128
+ - [Migrating from `beautiful-mermaid`](docs/migrating-from-beautiful-mermaid.md) — what's drop-in, which fixes change rendered output, and how to audit your own diagrams before upgrading
125
129
 
126
130
  More design write-ups and reference material live in [docs/](docs/).
127
131
 
@@ -144,11 +148,3 @@ Thank you Alexander for the excellent foundation!
144
148
  ## License
145
149
 
146
150
  MIT — see [LICENSE](LICENSE) for details.
147
-
148
- ---
149
-
150
- <div align="center">
151
-
152
- Built with care by the team at [Craft](https://craft.do)
153
-
154
- </div>
package/dist/cli.js CHANGED
@@ -800,13 +800,8 @@ function parseFlowchart(lines) {
800
800
  applyClickStatement(line, graph);
801
801
  continue;
802
802
  }
803
- const edgeMetaMatch = line.match(/^([\w-]+)(?=@\{)/);
804
- if (edgeMetaMatch && graph.edges.some((e) => e.id === edgeMetaMatch[1])) {
805
- const block = matchExpandedBlock(line.slice(edgeMetaMatch[1].length));
806
- if (block) {
807
- applyEdgeMeta(graph, edgeMetaMatch[1], parseExpandedMeta(block.body));
808
- continue;
809
- }
803
+ if (tryApplyEdgeMetaLine(line, graph)) {
804
+ continue;
810
805
  }
811
806
  const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);
812
807
  if (linkStyleMatch) {
@@ -920,6 +915,9 @@ function parseStateDiagram(lines) {
920
915
  }
921
916
  continue;
922
917
  }
918
+ if (tryApplyEdgeMetaLine(line, graph)) {
919
+ continue;
920
+ }
923
921
  const compositeMatch = line.match(
924
922
  /^state\s+(?:"([^"]+)"\s+as\s+)?([\w\p{L}]+)\s*\{$/u
925
923
  );
@@ -953,10 +951,11 @@ function parseStateDiagram(lines) {
953
951
  continue;
954
952
  }
955
953
  const transitionMatch = line.match(
956
- /^(\[\*\]|[\w\p{L}-]+)\s*(-->)\s*(\[\*\]|[\w\p{L}-]+)(?:\s*:\s*(.+))?$/u
954
+ /^(\[\*\]|[\w\p{L}-]+)\s*(?:([\w-]+)@)?-->\s*(\[\*\]|[\w\p{L}-]+)(?:\s*:\s*(.+))?$/u
957
955
  );
958
956
  if (transitionMatch) {
959
957
  let sourceId = transitionMatch[1];
958
+ const edgeId = transitionMatch[2];
960
959
  let targetId = transitionMatch[3];
961
960
  const rawTransitionLabel = transitionMatch[4]?.trim();
962
961
  const edgeLabel = rawTransitionLabel ? normalizeBrTags(rawTransitionLabel) : void 0;
@@ -988,7 +987,8 @@ function parseStateDiagram(lines) {
988
987
  label: edgeLabel,
989
988
  style: "solid",
990
989
  hasArrowStart: false,
991
- hasArrowEnd: true
990
+ hasArrowEnd: true,
991
+ ...edgeId !== void 0 ? { id: edgeId } : {}
992
992
  });
993
993
  continue;
994
994
  }
@@ -1162,6 +1162,17 @@ function applyClickStatement(line, graph) {
1162
1162
  graph.interactions.set(nodeId, interaction);
1163
1163
  }
1164
1164
  }
1165
+ var EDGE_META_ID_REGEX = /^([\w-]+)(?=@\{)/;
1166
+ function tryApplyEdgeMetaLine(line, graph) {
1167
+ const edgeMetaMatch = line.match(EDGE_META_ID_REGEX);
1168
+ if (!edgeMetaMatch || !graph.edges.some((e) => e.id === edgeMetaMatch[1])) {
1169
+ return false;
1170
+ }
1171
+ const block = matchExpandedBlock(line.slice(edgeMetaMatch[1].length));
1172
+ if (!block) return false;
1173
+ applyEdgeMeta(graph, edgeMetaMatch[1], parseExpandedMeta(block.body));
1174
+ return true;
1175
+ }
1165
1176
  function applyEdgeMeta(graph, edgeId, meta) {
1166
1177
  for (const edge of graph.edges) {
1167
1178
  if (edge.id !== edgeId) continue;
@@ -1539,11 +1550,32 @@ function isSafeCssVarReference(font) {
1539
1550
  }
1540
1551
  return depth === 0;
1541
1552
  }
1553
+ var GENERIC_FONT_FAMILIES = /* @__PURE__ */ new Set([
1554
+ "sans-serif",
1555
+ "serif",
1556
+ "monospace",
1557
+ "system-ui",
1558
+ "ui-sans-serif",
1559
+ "ui-serif",
1560
+ "ui-monospace",
1561
+ "ui-rounded",
1562
+ "cursive",
1563
+ "fantasy",
1564
+ "math",
1565
+ "emoji",
1566
+ "fangsong"
1567
+ ]);
1568
+ function isFontStackOrGenericFamily(font) {
1569
+ const trimmed = font.trim();
1570
+ if (trimmed.includes(",")) return true;
1571
+ return GENERIC_FONT_FAMILIES.has(trimmed.toLowerCase());
1572
+ }
1542
1573
  function buildStyleBlock(font, hasMonoFont) {
1543
1574
  const isVarReference = isSafeCssVarReference(font);
1544
1575
  const safeFont = isVarReference ? font.trim() : font.replace(/[<>{};'"]/g, "");
1576
+ const skipImport = isVarReference || isFontStackOrGenericFamily(font);
1545
1577
  const fontImports = [
1546
- ...isVarReference ? [] : [
1578
+ ...skipImport ? [] : [
1547
1579
  `@import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(safeFont)}:wght@400;500;600;700&amp;display=swap');`
1548
1580
  ],
1549
1581
  ...hasMonoFont ? [
@@ -1602,7 +1634,12 @@ function getReadableTextColor(fill, fallback) {
1602
1634
  const luminance = perceptualLuminance(...rgb);
1603
1635
  return luminance > 0.5 ? "#000000" : "#FFFFFF";
1604
1636
  }
1605
- function svgOpenTag(width, height, colors, transparent) {
1637
+ var titleIdCounter = 0;
1638
+ function nextTitleId() {
1639
+ titleIdCounter += 1;
1640
+ return `zm-title-${titleIdCounter}`;
1641
+ }
1642
+ function svgOpenTag(width, height, colors, transparent, title, decorative, hasInteractiveLinks) {
1606
1643
  const vars = [
1607
1644
  `--bg:${colors.bg}`,
1608
1645
  `--fg:${colors.fg}`,
@@ -1613,7 +1650,27 @@ function svgOpenTag(width, height, colors, transparent) {
1613
1650
  colors.border ? `--border:${colors.border}` : ""
1614
1651
  ].filter(Boolean).join(";");
1615
1652
  const bgStyle = transparent ? "" : ";background:var(--bg)";
1616
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" style="${vars}${bgStyle}">`;
1653
+ let a11yAttrs = "";
1654
+ let titleEl = "";
1655
+ if (hasInteractiveLinks) {
1656
+ if (title) {
1657
+ const id = nextTitleId();
1658
+ a11yAttrs = ` aria-labelledby="${id}"`;
1659
+ titleEl = `
1660
+ <title id="${id}">${escapeXml(title)}</title>`;
1661
+ }
1662
+ } else if (decorative) {
1663
+ a11yAttrs = ' aria-hidden="true"';
1664
+ } else {
1665
+ a11yAttrs = ' role="img"';
1666
+ if (title) {
1667
+ const id = nextTitleId();
1668
+ a11yAttrs += ` aria-labelledby="${id}"`;
1669
+ titleEl = `
1670
+ <title id="${id}">${escapeXml(title)}</title>`;
1671
+ }
1672
+ }
1673
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"${a11yAttrs} style="${vars}${bgStyle}">` + titleEl;
1617
1674
  }
1618
1675
 
1619
1676
  // src/ascii/ansi.ts
@@ -1887,19 +1944,44 @@ function colorizeText(text, hex, mode) {
1887
1944
 
1888
1945
  // src/ascii/display-width.ts
1889
1946
  var WIDE_CHAR_PLACEHOLDER = "";
1890
- function charDisplayWidth(char) {
1891
- return isWideChar(char) ? 2 : 1;
1947
+ var COMBINING_MARK_REGEX = /\p{Mn}|\p{Me}/u;
1948
+ function isCombiningMark2(char) {
1949
+ return COMBINING_MARK_REGEX.test(char);
1950
+ }
1951
+ var graphemeSegmenter = new Intl.Segmenter(void 0, {
1952
+ granularity: "grapheme"
1953
+ });
1954
+ function graphemeClusters(text) {
1955
+ const clusters = [];
1956
+ for (const { segment } of graphemeSegmenter.segment(text)) {
1957
+ clusters.push(segment);
1958
+ }
1959
+ return clusters;
1960
+ }
1961
+ function charDisplayWidth(cluster) {
1962
+ let sawWide = false;
1963
+ let sawNonMark = false;
1964
+ for (const ch of cluster) {
1965
+ if (isWideChar(ch)) sawWide = true;
1966
+ if (!isCombiningMark2(ch)) sawNonMark = true;
1967
+ }
1968
+ if (sawWide) return 2;
1969
+ return sawNonMark ? 1 : 0;
1892
1970
  }
1893
1971
  function displayWidth(text) {
1894
1972
  let width = 0;
1895
- for (const ch of text) width += charDisplayWidth(ch);
1973
+ for (const cluster of graphemeClusters(text)) {
1974
+ width += charDisplayWidth(cluster);
1975
+ }
1896
1976
  return width;
1897
1977
  }
1898
1978
  function toDisplayCells(text) {
1899
1979
  const cells = [];
1900
- for (const ch of text) {
1901
- cells.push(ch);
1902
- if (isWideChar(ch)) cells.push(WIDE_CHAR_PLACEHOLDER);
1980
+ for (const cluster of graphemeClusters(text)) {
1981
+ const width = charDisplayWidth(cluster);
1982
+ if (width === 0) continue;
1983
+ cells.push(cluster);
1984
+ if (width === 2) cells.push(WIDE_CHAR_PLACEHOLDER);
1903
1985
  }
1904
1986
  return cells;
1905
1987
  }
@@ -2257,10 +2339,28 @@ var Grid = class {
2257
2339
  add(key) {
2258
2340
  this.#cells.add(key);
2259
2341
  }
2342
+ /**
2343
+ * Release a single cell. For temporary reservations only (see
2344
+ * `rerouteAroundStyleConflicts` in grid.ts) — every other reservation in
2345
+ * this module (node blocks, in particular) is permanent for the life of
2346
+ * a render, so a real caller should rarely need this.
2347
+ */
2348
+ delete(key) {
2349
+ this.#cells.delete(key);
2350
+ }
2351
+ /** Iterate the reserved cell keys. Read-only — see `cloneGrid`. */
2352
+ keys() {
2353
+ return this.#cells.keys();
2354
+ }
2260
2355
  };
2261
2356
  function createGrid() {
2262
2357
  return new Grid();
2263
2358
  }
2359
+ function cloneGrid(grid) {
2360
+ const clone = createGrid();
2361
+ for (const key of grid.keys()) clone.add(key);
2362
+ return clone;
2363
+ }
2264
2364
  var NODE_BLOCK_SIZE = 3;
2265
2365
  function isOccupied(grid, coord) {
2266
2366
  return grid.has(gridKey(coord));
@@ -2292,6 +2392,42 @@ function placeBlock(grid, origin, size = NODE_BLOCK_SIZE) {
2292
2392
  }
2293
2393
  }
2294
2394
  }
2395
+ var MAX_WALK_STEPS = 1e5;
2396
+ function pathCells(path) {
2397
+ if (path.length === 0) return [];
2398
+ const cells = [path[0]];
2399
+ for (let i = 1; i < path.length; i++) {
2400
+ const from = path[i - 1];
2401
+ const to = path[i];
2402
+ let x = from.x;
2403
+ let y = from.y;
2404
+ const absDx = Math.abs(to.x - from.x);
2405
+ const absDy = Math.abs(to.y - from.y);
2406
+ const sx = from.x < to.x ? 1 : -1;
2407
+ const sy = from.y < to.y ? 1 : -1;
2408
+ let err = absDx - absDy;
2409
+ let steps = 0;
2410
+ while (x !== to.x || y !== to.y) {
2411
+ if (steps >= MAX_WALK_STEPS) {
2412
+ throw new Error(
2413
+ `pathCells: segment from (${from.x},${from.y}) to (${to.x},${to.y}) did not reach its endpoint within ${MAX_WALK_STEPS} steps`
2414
+ );
2415
+ }
2416
+ const e2 = 2 * err;
2417
+ if (e2 > -absDy) {
2418
+ err -= absDy;
2419
+ x += sx;
2420
+ }
2421
+ if (e2 < absDx) {
2422
+ err += absDx;
2423
+ y += sy;
2424
+ }
2425
+ cells.push({ x, y });
2426
+ steps++;
2427
+ }
2428
+ }
2429
+ return cells;
2430
+ }
2295
2431
 
2296
2432
  // src/ascii/converter.ts
2297
2433
  function convertToAsciiGraph(parsed, config) {
@@ -3107,6 +3243,26 @@ function processBundles(graph) {
3107
3243
  }
3108
3244
  }
3109
3245
 
3246
+ // src/ascii/edge-cell-styles.ts
3247
+ function createEdgeCellStyles() {
3248
+ return /* @__PURE__ */ new Map();
3249
+ }
3250
+ function findStyleConflict(grid, cellStyles, path, style) {
3251
+ for (const cell of pathCells(path)) {
3252
+ if (isOccupied(grid, cell)) continue;
3253
+ const existing = cellStyles.get(gridKey(cell));
3254
+ if (existing !== void 0 && existing !== style) return cell;
3255
+ }
3256
+ return null;
3257
+ }
3258
+ function claimPathCells(grid, cellStyles, path, style) {
3259
+ for (const cell of pathCells(path)) {
3260
+ if (isOccupied(grid, cell)) continue;
3261
+ const key = gridKey(cell);
3262
+ if (!cellStyles.has(key)) cellStyles.set(key, style);
3263
+ }
3264
+ }
3265
+
3110
3266
  // src/ascii/multiline-utils.ts
3111
3267
  function splitLines(label) {
3112
3268
  return label.split("\n");
@@ -4882,6 +5038,26 @@ function increaseGridSizeForPath(graph, path) {
4882
5038
  }
4883
5039
  }
4884
5040
  }
5041
+ var MAX_STYLE_CONFLICT_REROUTES = 8;
5042
+ function rerouteAroundStyleConflicts(graph, edge, cellStyles, nodeOnlyGrid) {
5043
+ const temporarilyBlocked = [];
5044
+ try {
5045
+ for (let i = 0; i < MAX_STYLE_CONFLICT_REROUTES; i++) {
5046
+ const conflict = findStyleConflict(
5047
+ nodeOnlyGrid,
5048
+ cellStyles,
5049
+ edge.path,
5050
+ edge.style
5051
+ );
5052
+ if (!conflict) return;
5053
+ graph.grid.add(gridKey(conflict));
5054
+ temporarilyBlocked.push(conflict);
5055
+ determinePath(graph, edge);
5056
+ }
5057
+ } finally {
5058
+ for (const cell of temporarilyBlocked) graph.grid.delete(gridKey(cell));
5059
+ }
5060
+ }
4885
5061
  function isNodeInAnySubgraph(graph, node) {
4886
5062
  return graph.subgraphs.some((sg) => sg.nodes.includes(node));
4887
5063
  }
@@ -5236,14 +5412,19 @@ function createMapping(graph) {
5236
5412
  graph.pathBudget = createPathBudget();
5237
5413
  graph.bundles = analyzeEdgeBundles(graph);
5238
5414
  processBundles(graph);
5415
+ const cellStyles = createEdgeCellStyles();
5416
+ const nodeOnlyGrid = cloneGrid(graph.grid);
5239
5417
  for (const edge of graph.edges) {
5240
5418
  if (edge.bundle && edge.path.length > 0) {
5241
5419
  increaseGridSizeForPath(graph, edge.path);
5420
+ claimPathCells(nodeOnlyGrid, cellStyles, edge.path, edge.style);
5242
5421
  determineLabelLine(graph, edge);
5243
5422
  continue;
5244
5423
  }
5245
5424
  determinePath(graph, edge);
5425
+ rerouteAroundStyleConflicts(graph, edge, cellStyles, nodeOnlyGrid);
5246
5426
  increaseGridSizeForPath(graph, edge.path);
5427
+ claimPathCells(nodeOnlyGrid, cellStyles, edge.path, edge.style);
5247
5428
  determineLabelLine(graph, edge);
5248
5429
  }
5249
5430
  for (const node of graph.nodes) {
@@ -9211,11 +9392,27 @@ function pointsToPath(points, curve = "linear") {
9211
9392
  }
9212
9393
 
9213
9394
  // src/renderer.ts
9214
- function renderSvg(graph, colors, font = "Inter", transparent = false, fontSizes = FONT_SIZES, curve = "linear") {
9395
+ function renderSvg(graph, colors, font = "Inter", transparent = false, fontSizes = FONT_SIZES, curve = "linear", embedSource, animationEnabled = true, title, decorative) {
9215
9396
  const parts = [];
9216
- parts.push(svgOpenTag(graph.width, graph.height, colors, transparent));
9397
+ const hasInteractiveLinks = graph.nodes.some(
9398
+ (n) => Boolean(safeHref(n.interaction?.href))
9399
+ );
9400
+ parts.push(
9401
+ withDataSrc(
9402
+ svgOpenTag(
9403
+ graph.width,
9404
+ graph.height,
9405
+ colors,
9406
+ transparent,
9407
+ title,
9408
+ decorative,
9409
+ hasInteractiveLinks
9410
+ ),
9411
+ embedSource
9412
+ )
9413
+ );
9217
9414
  parts.push(buildStyleBlock(font, false));
9218
- if (graph.edges.some((e) => e.animate)) {
9415
+ if (animationEnabled && graph.edges.some((e) => e.animate)) {
9219
9416
  parts.push(edgeAnimationStyle());
9220
9417
  }
9221
9418
  parts.push("<defs>");
@@ -9234,7 +9431,7 @@ function renderSvg(graph, colors, font = "Inter", transparent = false, fontSizes
9234
9431
  parts.push(renderGroup(group, font, fontSizes));
9235
9432
  }
9236
9433
  for (const edge of graph.edges) {
9237
- parts.push(renderEdge(edge, curve));
9434
+ parts.push(renderEdge(edge, curve, animationEnabled));
9238
9435
  }
9239
9436
  for (const edge of graph.edges) {
9240
9437
  if (edge.label) {
@@ -9305,8 +9502,9 @@ function renderGroup(group, font, fontSizes) {
9305
9502
  parts.push("</g>");
9306
9503
  return parts.join("\n");
9307
9504
  }
9308
- function renderEdge(edge, curve) {
9505
+ function renderEdge(edge, curve, animationEnabled = true) {
9309
9506
  if (edge.points.length < 2) return "";
9507
+ const animate = animationEnabled && (edge.animate ?? false);
9310
9508
  const curved = curve !== "linear";
9311
9509
  const invisible = edge.style === "invisible";
9312
9510
  const dashArray = edge.style === "dotted" ? ' stroke-dasharray="4 4"' : "";
@@ -9323,9 +9521,9 @@ function renderEdge(edge, curve) {
9323
9521
  markers += ` marker-start="url(#arrowhead-start${suffix})"`;
9324
9522
  }
9325
9523
  const dataAttrs = [
9326
- // An animated edge (`e1@{ animate: true }`) carries an extra class; its
9327
- // keyframes live in the shared style block.
9328
- `class="edge${edge.animate ? " edge-animated" : ""}"`,
9524
+ // An animated edge (`e1@{ animate: true }`, with animation enabled)
9525
+ // carries an extra class; its keyframes live in the shared style block.
9526
+ `class="edge${animate ? " edge-animated" : ""}"`,
9329
9527
  `data-from="${escapeAttr(edge.source)}"`,
9330
9528
  `data-to="${escapeAttr(edge.target)}"`,
9331
9529
  `data-style="${edge.style}"`,
@@ -9338,7 +9536,7 @@ function renderEdge(edge, curve) {
9338
9536
  if (edge.id) {
9339
9537
  dataAttrs.push(`data-id="${escapeAttr(edge.id)}"`);
9340
9538
  }
9341
- const animatedDash = edge.animate && !dashArray ? ' stroke-dasharray="8 6"' : "";
9539
+ const animatedDash = animate && !dashArray ? ' stroke-dasharray="8 6"' : "";
9342
9540
  const geometry = curved ? `d="${pointsToPath(edge.points, curve)}"` : `points="${pointsToPolylinePath(edge.points)}"`;
9343
9541
  return `<${curved ? "path" : "polyline"} ${dataAttrs.join(" ")} ${geometry} fill="none" stroke="${strokeColor}" stroke-width="${strokeWidth}"${dashArray}${animatedDash}${markers} />`;
9344
9542
  }
@@ -9865,6 +10063,16 @@ function renderNodeLabel(node, _font, fontSizes) {
9865
10063
  function escapeAttr(value) {
9866
10064
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9867
10065
  }
10066
+ function escapeMultilineAttr(value) {
10067
+ return escapeAttr(value).replace(/\r/g, "&#13;").replace(/\n/g, "&#10;").replace(/\t/g, "&#9;");
10068
+ }
10069
+ function withDataSrc(svgTag, source) {
10070
+ if (source === void 0) return svgTag;
10071
+ return svgTag.replace(
10072
+ "<svg ",
10073
+ `<svg data-src="${escapeMultilineAttr(source)}" `
10074
+ );
10075
+ }
9868
10076
  function sanitizeClassName(className) {
9869
10077
  if (!className) return void 0;
9870
10078
  return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(className) ? className : void 0;
@@ -10243,9 +10451,21 @@ function layoutSequenceDiagram(diagram, options = {}) {
10243
10451
  }
10244
10452
 
10245
10453
  // src/sequence/renderer.ts
10246
- function renderSequenceSvg(diagram, colors, font = "Inter", transparent = false, fontSizes = FONT_SIZES) {
10454
+ function renderSequenceSvg(diagram, colors, font = "Inter", transparent = false, fontSizes = FONT_SIZES, embedSource, title, decorative) {
10247
10455
  const parts = [];
10248
- parts.push(svgOpenTag(diagram.width, diagram.height, colors, transparent));
10456
+ parts.push(
10457
+ withDataSrc(
10458
+ svgOpenTag(
10459
+ diagram.width,
10460
+ diagram.height,
10461
+ colors,
10462
+ transparent,
10463
+ title,
10464
+ decorative
10465
+ ),
10466
+ embedSource
10467
+ )
10468
+ );
10249
10469
  parts.push(buildStyleBlock(font, false));
10250
10470
  parts.push("<defs>");
10251
10471
  parts.push(arrowMarkerDefs2());
@@ -10639,9 +10859,21 @@ var CLS_FONT = {
10639
10859
  annotationSize: 10,
10640
10860
  annotationWeight: 500
10641
10861
  };
10642
- function renderClassSvg(diagram, colors, font = "Inter", transparent = false, fontSizes = FONT_SIZES) {
10862
+ function renderClassSvg(diagram, colors, font = "Inter", transparent = false, fontSizes = FONT_SIZES, embedSource, title, decorative) {
10643
10863
  const parts = [];
10644
- parts.push(svgOpenTag(diagram.width, diagram.height, colors, transparent));
10864
+ parts.push(
10865
+ withDataSrc(
10866
+ svgOpenTag(
10867
+ diagram.width,
10868
+ diagram.height,
10869
+ colors,
10870
+ transparent,
10871
+ title,
10872
+ decorative
10873
+ ),
10874
+ embedSource
10875
+ )
10876
+ );
10645
10877
  parts.push(buildStyleBlock(font, true));
10646
10878
  parts.push("<defs>");
10647
10879
  parts.push(relationshipMarkerDefs());
@@ -11022,9 +11254,21 @@ var ER_FONT = {
11022
11254
  keySize: 9,
11023
11255
  keyWeight: 600
11024
11256
  };
11025
- function renderErSvg(diagram, colors, font = "Inter", transparent = false, fontSizes = FONT_SIZES) {
11257
+ function renderErSvg(diagram, colors, font = "Inter", transparent = false, fontSizes = FONT_SIZES, embedSource, title, decorative) {
11026
11258
  const parts = [];
11027
- parts.push(svgOpenTag(diagram.width, diagram.height, colors, transparent));
11259
+ parts.push(
11260
+ withDataSrc(
11261
+ svgOpenTag(
11262
+ diagram.width,
11263
+ diagram.height,
11264
+ colors,
11265
+ transparent,
11266
+ title,
11267
+ decorative
11268
+ ),
11269
+ embedSource
11270
+ )
11271
+ );
11028
11272
  parts.push(buildStyleBlock(font, true));
11029
11273
  parts.push("<defs>");
11030
11274
  parts.push("</defs>");
@@ -11756,19 +12000,24 @@ var TIP = {
11756
12000
  minY: 4,
11757
12001
  pointerSize: 6
11758
12002
  };
11759
- function renderXYChartSvg(chart, colors, font = "Inter", transparent = false, interactive = false) {
12003
+ function renderXYChartSvg(chart, colors, font = "Inter", transparent = false, interactive = false, embedSource, title, decorative) {
11760
12004
  const parts = [];
11761
12005
  const maxColorIdx = Math.max(
11762
12006
  0,
11763
12007
  ...chart.bars.map((b) => b.colorIndex),
11764
12008
  ...chart.lines.map((l) => l.colorIndex)
11765
12009
  );
11766
- const svgTag = svgOpenTag(
11767
- chart.width,
11768
- chart.height,
11769
- colors,
11770
- transparent
11771
- ).replace("<svg ", `<svg data-xychart-colors="${maxColorIdx}" `);
12010
+ const svgTag = withDataSrc(
12011
+ svgOpenTag(
12012
+ chart.width,
12013
+ chart.height,
12014
+ colors,
12015
+ transparent,
12016
+ title,
12017
+ decorative
12018
+ ).replace("<svg ", `<svg data-xychart-colors="${maxColorIdx}" `),
12019
+ embedSource
12020
+ );
11772
12021
  parts.push(svgTag);
11773
12022
  parts.push(buildStyleBlock(font, false));
11774
12023
  const maxLinePoints = Math.max(...chart.lines.map((l) => l.points.length), 0);
@@ -12182,29 +12431,72 @@ function buildColors(options) {
12182
12431
  border: options.border
12183
12432
  };
12184
12433
  }
12434
+ function resolveInteractivity(options) {
12435
+ return options.interactivity ?? "static";
12436
+ }
12437
+ function resolveAnimationEnabled(options) {
12438
+ return resolveInteractivity(options) !== "none";
12439
+ }
12440
+ function resolveXYChartInteractive(options) {
12441
+ if (options.interactivity !== void 0) {
12442
+ return options.interactivity === "full";
12443
+ }
12444
+ return options.interactive ?? false;
12445
+ }
12185
12446
  function renderMermaidSVG(text, options = {}) {
12447
+ const originalText = text;
12186
12448
  text = decodeXML(text);
12187
12449
  const colors = buildColors(options);
12188
12450
  const font = options.font ?? "Inter";
12189
12451
  const transparent = options.transparent ?? false;
12190
12452
  const fontSizes = resolveFontSizes(options.fontSizes);
12191
12453
  const diagramType = detectDiagramType(text);
12454
+ const embedSource = options.embedSource ? originalText : void 0;
12455
+ const title = options.title;
12456
+ const decorative = options.decorative;
12192
12457
  const lines = splitStatements(text);
12193
12458
  switch (diagramType) {
12194
12459
  case "sequence": {
12195
12460
  const diagram = parseSequenceDiagram(lines);
12196
12461
  const positioned = layoutSequenceDiagram(diagram, options);
12197
- return renderSequenceSvg(positioned, colors, font, transparent, fontSizes);
12462
+ return renderSequenceSvg(
12463
+ positioned,
12464
+ colors,
12465
+ font,
12466
+ transparent,
12467
+ fontSizes,
12468
+ embedSource,
12469
+ title,
12470
+ decorative
12471
+ );
12198
12472
  }
12199
12473
  case "class": {
12200
12474
  const diagram = parseClassDiagram(lines);
12201
12475
  const positioned = layoutClassDiagramSync(diagram, options);
12202
- return renderClassSvg(positioned, colors, font, transparent, fontSizes);
12476
+ return renderClassSvg(
12477
+ positioned,
12478
+ colors,
12479
+ font,
12480
+ transparent,
12481
+ fontSizes,
12482
+ embedSource,
12483
+ title,
12484
+ decorative
12485
+ );
12203
12486
  }
12204
12487
  case "er": {
12205
12488
  const diagram = parseErDiagram(lines);
12206
12489
  const positioned = layoutErDiagramSync(diagram, options);
12207
- return renderErSvg(positioned, colors, font, transparent, fontSizes);
12490
+ return renderErSvg(
12491
+ positioned,
12492
+ colors,
12493
+ font,
12494
+ transparent,
12495
+ fontSizes,
12496
+ embedSource,
12497
+ title,
12498
+ decorative
12499
+ );
12208
12500
  }
12209
12501
  case "xychart": {
12210
12502
  const chart = parseXYChart(lines);
@@ -12214,7 +12506,10 @@ function renderMermaidSVG(text, options = {}) {
12214
12506
  colors,
12215
12507
  font,
12216
12508
  transparent,
12217
- options.interactive ?? false
12509
+ resolveXYChartInteractive(options),
12510
+ embedSource,
12511
+ title,
12512
+ decorative
12218
12513
  );
12219
12514
  }
12220
12515
  case "flowchart":
@@ -12228,7 +12523,11 @@ function renderMermaidSVG(text, options = {}) {
12228
12523
  font,
12229
12524
  transparent,
12230
12525
  fontSizes,
12231
- effective.curve ?? "linear"
12526
+ effective.curve ?? "linear",
12527
+ embedSource,
12528
+ resolveAnimationEnabled(options),
12529
+ title,
12530
+ decorative
12232
12531
  );
12233
12532
  }
12234
12533
  }