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.
- package/README.md +6 -10
- package/dist/cli.js +344 -45
- package/dist/index.cjs +540 -241
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -1
- package/dist/index.d.ts +90 -1
- package/dist/index.js +540 -241
- package/dist/index.js.map +1 -1
- package/package.json +7 -1
- package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
- package/src/__tests__/ascii-display-width-unit.test.ts +129 -0
- package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +212 -0
- package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
- package/src/__tests__/edge-cell-styles.test.ts +177 -0
- package/src/__tests__/grid-occupancy.test.ts +160 -0
- package/src/__tests__/helpers/terminal-display-width.ts +41 -13
- package/src/__tests__/integration.test.ts +107 -0
- package/src/__tests__/interactivity-config.test.ts +64 -0
- package/src/__tests__/interactivity-option.test.ts +141 -0
- package/src/__tests__/sequence-integration.test.ts +28 -0
- package/src/__tests__/svg-accessible-name.test.ts +392 -0
- package/src/__tests__/theme.test.ts +24 -0
- package/src/ascii/display-width.ts +92 -26
- package/src/ascii/edge-cell-styles.ts +81 -0
- package/src/ascii/grid-occupancy.ts +98 -0
- package/src/ascii/grid.ts +102 -3
- package/src/class/renderer.ts +23 -1
- package/src/er/renderer.ts +23 -1
- package/src/index.ts +99 -4
- package/src/parser.ts +52 -16
- package/src/renderer.ts +95 -11
- package/src/sequence/renderer.ts +23 -1
- package/src/theme.ts +153 -2
- package/src/types.ts +76 -1
- 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
|
-

|
|
10
10
|
|
|
11
|
-
[
|
|
11
|
+
[](https://dfadler.github.io/zombie-mermaid/)
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/zombie-mermaid)
|
|
14
|
+
[](https://github.com/dfadler/zombie-mermaid/actions/workflows/ci.yml)
|
|
15
|
+
[](https://codecov.io/gh/dfadler/zombie-mermaid)
|
|
16
|
+
[](https://socket.dev/npm/package/zombie-mermaid)
|
|
14
17
|
[](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
|
-
|
|
804
|
-
|
|
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*(
|
|
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
|
-
...
|
|
1578
|
+
...skipImport ? [] : [
|
|
1547
1579
|
`@import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(safeFont)}:wght@400;500;600;700&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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1891
|
-
|
|
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
|
|
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
|
|
1901
|
-
|
|
1902
|
-
if (
|
|
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
|
-
|
|
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 }
|
|
9327
|
-
// keyframes live in the shared style block.
|
|
9328
|
-
`class="edge${
|
|
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 =
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
9867
10065
|
}
|
|
10066
|
+
function escapeMultilineAttr(value) {
|
|
10067
|
+
return escapeAttr(value).replace(/\r/g, " ").replace(/\n/g, " ").replace(/\t/g, "	");
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
}
|