@ktrysmt/beautiful-mermaid 1.4.1 → 1.4.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.
- package/dist/index.js +133 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/fan-in-layout.test.ts +109 -0
- package/src/__tests__/level-assignment.test.ts +103 -0
- package/src/__tests__/parser.test.ts +87 -0
- package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +7 -15
- package/src/__tests__/testdata/ascii/backlink_from_top.txt +13 -13
- package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +11 -12
- package/src/__tests__/testdata/ascii/comments.txt +7 -15
- package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +21 -21
- package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +7 -15
- package/src/__tests__/testdata/unicode/backlink_from_top.txt +13 -13
- package/src/__tests__/testdata/unicode/comments.txt +7 -15
- package/src/ascii/edge-routing.ts +77 -26
- package/src/ascii/grid.ts +53 -3
- package/src/ascii/pathfinder.ts +8 -1
- package/src/ascii/types.ts +2 -0
- package/src/multiline-utils.ts +13 -3
- package/src/parser.ts +69 -4
package/dist/index.js
CHANGED
|
@@ -263,8 +263,11 @@ function measureMultilineText(text, fontSize, fontWeight) {
|
|
|
263
263
|
|
|
264
264
|
// src/multiline-utils.ts
|
|
265
265
|
function normalizeBrTags(label) {
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
let result = label.replace(/<br\s*\/?>/gi, "\n").replace(/\\n/g, "\n").trim();
|
|
267
|
+
if (result.startsWith('"') && result.endsWith('"')) {
|
|
268
|
+
result = result.slice(1, -1);
|
|
269
|
+
}
|
|
270
|
+
return result.replace(/<\/?(?:sub|sup|small|mark)\s*>/gi, "").replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, "<i>$1</i>").replace(/~~(.+?)~~/g, "<s>$1</s>").trim();
|
|
268
271
|
}
|
|
269
272
|
function escapeXml(text) {
|
|
270
273
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -336,8 +339,43 @@ ${textEl}`;
|
|
|
336
339
|
}
|
|
337
340
|
|
|
338
341
|
// src/parser.ts
|
|
342
|
+
function joinMultilineDefinitions(lines) {
|
|
343
|
+
const result = [];
|
|
344
|
+
let buffer = "";
|
|
345
|
+
let bracketDepth = 0;
|
|
346
|
+
let parenDepth = 0;
|
|
347
|
+
let inQuote = false;
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
if (bracketDepth === 0 && parenDepth === 0 && !inQuote) {
|
|
350
|
+
buffer = line;
|
|
351
|
+
} else {
|
|
352
|
+
if (/<br\s*\/?>\s*$/i.test(buffer)) {
|
|
353
|
+
buffer += line;
|
|
354
|
+
} else {
|
|
355
|
+
buffer += "<br>" + line;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const ch of line) {
|
|
359
|
+
if (ch === '"') {
|
|
360
|
+
inQuote = !inQuote;
|
|
361
|
+
} else if (!inQuote) {
|
|
362
|
+
if (ch === "[") bracketDepth++;
|
|
363
|
+
else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
364
|
+
else if (ch === "(") parenDepth++;
|
|
365
|
+
else if (ch === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (bracketDepth === 0 && parenDepth === 0 && !inQuote) {
|
|
369
|
+
result.push(buffer);
|
|
370
|
+
buffer = "";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (buffer) result.push(buffer);
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
339
376
|
function parseMermaid(text) {
|
|
340
|
-
const
|
|
377
|
+
const rawLines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("%%"));
|
|
378
|
+
const lines = joinMultilineDefinitions(rawLines);
|
|
341
379
|
if (lines.length === 0) {
|
|
342
380
|
throw new Error("Empty mermaid diagram");
|
|
343
381
|
}
|
|
@@ -416,7 +454,7 @@ function parseFlowchart(lines) {
|
|
|
416
454
|
const subgraphMatch = line.match(/^subgraph\s+(.+)$/);
|
|
417
455
|
if (subgraphMatch) {
|
|
418
456
|
const rest = subgraphMatch[1].trim();
|
|
419
|
-
const bracketMatch = rest.match(/^([\w-]+)\s*\[(.+)\]$/);
|
|
457
|
+
const bracketMatch = rest.match(/^([\w\p{L}-]+)\s*\[(.+)\]$/u);
|
|
420
458
|
let id;
|
|
421
459
|
let label;
|
|
422
460
|
if (bracketMatch) {
|
|
@@ -424,7 +462,7 @@ function parseFlowchart(lines) {
|
|
|
424
462
|
label = normalizeBrTags(bracketMatch[2]);
|
|
425
463
|
} else {
|
|
426
464
|
label = normalizeBrTags(rest);
|
|
427
|
-
id = rest.replace(/\s+/g, "_").replace(/[^\w]/
|
|
465
|
+
id = rest.replace(/\s+/g, "_").replace(/[^\p{L}\p{N}\w]/gu, "");
|
|
428
466
|
}
|
|
429
467
|
const sg = { id, label, nodeIds: [], children: [] };
|
|
430
468
|
subgraphStack.push(sg);
|
|
@@ -1691,7 +1729,7 @@ function isFreeInGrid(grid, c) {
|
|
|
1691
1729
|
return !grid.has(gridKey(c));
|
|
1692
1730
|
}
|
|
1693
1731
|
var MAX_ITERATIONS = 5e4;
|
|
1694
|
-
function getPath(grid, from, to) {
|
|
1732
|
+
function getPath(grid, from, to, congestion) {
|
|
1695
1733
|
const pq = new MinHeap();
|
|
1696
1734
|
pq.push({ coord: from, priority: 0 });
|
|
1697
1735
|
const costSoFar = /* @__PURE__ */ new Map();
|
|
@@ -1719,7 +1757,8 @@ function getPath(grid, from, to) {
|
|
|
1719
1757
|
if (!isFreeInGrid(grid, next) && !gridCoordEquals(next, to)) {
|
|
1720
1758
|
continue;
|
|
1721
1759
|
}
|
|
1722
|
-
const
|
|
1760
|
+
const congestionPenalty = congestion?.get(gridKey(next)) ?? 0;
|
|
1761
|
+
const newCost = currentCost + 1 + congestionPenalty * 3;
|
|
1723
1762
|
const nextKey = gridKey(next);
|
|
1724
1763
|
const existingCost = costSoFar.get(nextKey);
|
|
1725
1764
|
if (existingCost === void 0 || newCost < existingCost) {
|
|
@@ -1863,17 +1902,17 @@ function determineStartAndEndDir(edge, graphDirection) {
|
|
|
1863
1902
|
}
|
|
1864
1903
|
return [preferredDir, preferredOppositeDir, alternativeDir, alternativeOppositeDir];
|
|
1865
1904
|
}
|
|
1866
|
-
function determinePath(graph, edge) {
|
|
1905
|
+
function determinePath(graph, edge, congestion) {
|
|
1867
1906
|
const sourceSg = getNodeSubgraph(graph, edge.from);
|
|
1868
1907
|
const targetSg = getNodeSubgraph(graph, edge.to);
|
|
1869
1908
|
const effectiveDir = sourceSg && sourceSg === targetSg && sourceSg.direction ? sourceSg.direction : graph.config.graphDirection;
|
|
1870
1909
|
const [preferredDir, preferredOppositeDir, alternativeDir, alternativeOppositeDir] = determineStartAndEndDir(edge, effectiveDir);
|
|
1871
1910
|
const prefFrom = gridCoordDirection(edge.from.gridCoord, preferredDir);
|
|
1872
1911
|
const prefTo = gridCoordDirection(edge.to.gridCoord, preferredOppositeDir);
|
|
1873
|
-
let preferredPath = getPath(graph.grid, prefFrom, prefTo);
|
|
1912
|
+
let preferredPath = getPath(graph.grid, prefFrom, prefTo, congestion);
|
|
1874
1913
|
const altFrom = gridCoordDirection(edge.from.gridCoord, alternativeDir);
|
|
1875
1914
|
const altTo = gridCoordDirection(edge.to.gridCoord, alternativeOppositeDir);
|
|
1876
|
-
let alternativePath = getPath(graph.grid, altFrom, altTo);
|
|
1915
|
+
let alternativePath = getPath(graph.grid, altFrom, altTo, congestion);
|
|
1877
1916
|
if (preferredPath !== null && alternativePath !== null) {
|
|
1878
1917
|
preferredPath = mergePath(preferredPath);
|
|
1879
1918
|
alternativePath = mergePath(alternativePath);
|
|
@@ -1904,11 +1943,41 @@ function determinePath(graph, edge) {
|
|
|
1904
1943
|
edge.endDir = preferredOppositeDir;
|
|
1905
1944
|
edge.path = [prefFrom, prefTo];
|
|
1906
1945
|
}
|
|
1946
|
+
function pathToCells(path) {
|
|
1947
|
+
const cells = [];
|
|
1948
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1949
|
+
const p1 = path[i];
|
|
1950
|
+
const p2 = path[i + 1];
|
|
1951
|
+
if (p1.x === p2.x) {
|
|
1952
|
+
const minY = Math.min(p1.y, p2.y);
|
|
1953
|
+
const maxY = Math.max(p1.y, p2.y);
|
|
1954
|
+
for (let y = minY; y <= maxY; y++) {
|
|
1955
|
+
cells.push({ x: p1.x, y });
|
|
1956
|
+
}
|
|
1957
|
+
} else {
|
|
1958
|
+
const minX = Math.min(p1.x, p2.x);
|
|
1959
|
+
const maxX = Math.max(p1.x, p2.x);
|
|
1960
|
+
for (let x = minX; x <= maxX; x++) {
|
|
1961
|
+
cells.push({ x, y: p1.y });
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return cells;
|
|
1966
|
+
}
|
|
1967
|
+
function segmentMidpointKey(line) {
|
|
1968
|
+
const isVertical = line[0].x === line[1].x;
|
|
1969
|
+
if (isVertical) {
|
|
1970
|
+
const mid = Math.floor((line[0].y + line[1].y) / 2);
|
|
1971
|
+
return `v:${line[0].x}:${mid}`;
|
|
1972
|
+
} else {
|
|
1973
|
+
const mid = Math.floor((line[0].x + line[1].x) / 2);
|
|
1974
|
+
return `h:${line[0].y}:${mid}`;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1907
1977
|
function determineLabelLine(graph, edge) {
|
|
1908
1978
|
if (edge.text.length === 0) return;
|
|
1909
1979
|
const lenLabel = maxLineWidth(edge.text);
|
|
1910
1980
|
const pathLen = edge.path.length;
|
|
1911
|
-
const isVerticalFlow = graph.config.graphDirection === "TD";
|
|
1912
1981
|
const segments = [];
|
|
1913
1982
|
for (let i = 1; i < pathLen; i++) {
|
|
1914
1983
|
const p1 = edge.path[i - 1];
|
|
@@ -1918,21 +1987,26 @@ function determineLabelLine(graph, edge) {
|
|
|
1918
1987
|
const isVertical = p1.x === p2.x;
|
|
1919
1988
|
segments.push({ line, width, index: i, isVertical });
|
|
1920
1989
|
}
|
|
1921
|
-
const
|
|
1990
|
+
const used = graph.usedLabelMidpoints ?? /* @__PURE__ */ new Set();
|
|
1991
|
+
const middleIdx = Math.floor(pathLen / 2);
|
|
1992
|
+
const scoreSeg = (s) => {
|
|
1993
|
+
const key = segmentMidpointKey(s.line);
|
|
1994
|
+
let score = 0;
|
|
1995
|
+
if (!used.has(key)) score += 1e3;
|
|
1996
|
+
if (s.index > 1) score += 100;
|
|
1997
|
+
score -= Math.abs(s.index - middleIdx);
|
|
1998
|
+
return score;
|
|
1999
|
+
};
|
|
2000
|
+
const candidates = segments.filter((s) => s.width >= lenLabel);
|
|
1922
2001
|
let largestLine;
|
|
1923
|
-
if (
|
|
1924
|
-
|
|
1925
|
-
largestLine =
|
|
2002
|
+
if (candidates.length > 0) {
|
|
2003
|
+
candidates.sort((a, b) => scoreSeg(b) - scoreSeg(a));
|
|
2004
|
+
largestLine = candidates[0].line;
|
|
1926
2005
|
} else {
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
fallbackSegments.sort((a, b) => b.index - a.index);
|
|
1930
|
-
largestLine = fallbackSegments[0].line;
|
|
1931
|
-
} else {
|
|
1932
|
-
segments.sort((a, b) => b.width - a.width);
|
|
1933
|
-
largestLine = segments[0]?.line ?? [edge.path[0], edge.path[1]];
|
|
1934
|
-
}
|
|
2006
|
+
segments.sort((a, b) => b.width - a.width);
|
|
2007
|
+
largestLine = segments[0]?.line ?? [edge.path[0], edge.path[1]];
|
|
1935
2008
|
}
|
|
2009
|
+
used.add(segmentMidpointKey(largestLine));
|
|
1936
2010
|
const minX = Math.min(largestLine[0].x, largestLine[1].x);
|
|
1937
2011
|
const maxX = Math.max(largestLine[0].x, largestLine[1].x);
|
|
1938
2012
|
const middleX = minX + Math.floor((maxX - minX) / 2);
|
|
@@ -3815,7 +3889,14 @@ function createMapping(graph) {
|
|
|
3815
3889
|
const parentSg = getNodeSubgraph(graph, node);
|
|
3816
3890
|
const childSg = getNodeSubgraph(graph, child);
|
|
3817
3891
|
const edgeDir = parentSg && parentSg === childSg && parentSg.direction ? parentSg.direction : graph.config.graphDirection;
|
|
3818
|
-
let
|
|
3892
|
+
let maxPredLevel = edgeDir === "LR" ? gc.x : gc.y;
|
|
3893
|
+
for (const e of graph.edges) {
|
|
3894
|
+
if (e.to.name === child.name && e.from.gridCoord !== null) {
|
|
3895
|
+
const predLevel = edgeDir === "LR" ? e.from.gridCoord.x : e.from.gridCoord.y;
|
|
3896
|
+
if (predLevel > maxPredLevel) maxPredLevel = predLevel;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
let childLevel = maxPredLevel + 4;
|
|
3819
3900
|
if (childSg && parentSg !== childSg && deferredRoots.size > 0) {
|
|
3820
3901
|
let placedDeferred = false;
|
|
3821
3902
|
for (const dr of [...deferredRoots]) {
|
|
@@ -3880,17 +3961,44 @@ function createMapping(graph) {
|
|
|
3880
3961
|
for (const node of graph.nodes) {
|
|
3881
3962
|
setColumnWidth(graph, node);
|
|
3882
3963
|
}
|
|
3964
|
+
const labeledInDegree = /* @__PURE__ */ new Map();
|
|
3965
|
+
for (const edge of graph.edges) {
|
|
3966
|
+
if (edge.text.length > 0) {
|
|
3967
|
+
labeledInDegree.set(edge.to.name, (labeledInDegree.get(edge.to.name) ?? 0) + 1);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
for (const node of graph.nodes) {
|
|
3971
|
+
const deg = labeledInDegree.get(node.name) ?? 0;
|
|
3972
|
+
if (deg >= 3 && node.gridCoord) {
|
|
3973
|
+
const approachRow = node.gridCoord.y - 1;
|
|
3974
|
+
if (approachRow >= 0) {
|
|
3975
|
+
const extra = Math.min((deg - 2) * 2, 10);
|
|
3976
|
+
const current = graph.rowHeight.get(approachRow) ?? 0;
|
|
3977
|
+
graph.rowHeight.set(approachRow, Math.max(current, current + extra));
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3883
3981
|
graph.bundles = analyzeEdgeBundles(graph);
|
|
3884
3982
|
processBundles(graph);
|
|
3983
|
+
graph.usedLabelMidpoints = /* @__PURE__ */ new Set();
|
|
3984
|
+
const congestion = /* @__PURE__ */ new Map();
|
|
3885
3985
|
for (const edge of graph.edges) {
|
|
3886
3986
|
if (edge.bundle && edge.path.length > 0) {
|
|
3887
3987
|
increaseGridSizeForPath(graph, edge.path);
|
|
3888
3988
|
determineLabelLine(graph, edge);
|
|
3989
|
+
for (const cell of pathToCells(edge.path)) {
|
|
3990
|
+
const k = gridKey(cell);
|
|
3991
|
+
congestion.set(k, (congestion.get(k) ?? 0) + 1);
|
|
3992
|
+
}
|
|
3889
3993
|
continue;
|
|
3890
3994
|
}
|
|
3891
|
-
determinePath(graph, edge);
|
|
3995
|
+
determinePath(graph, edge, congestion);
|
|
3892
3996
|
increaseGridSizeForPath(graph, edge.path);
|
|
3893
3997
|
determineLabelLine(graph, edge);
|
|
3998
|
+
for (const cell of pathToCells(edge.path)) {
|
|
3999
|
+
const k = gridKey(cell);
|
|
4000
|
+
congestion.set(k, (congestion.get(k) ?? 0) + 1);
|
|
4001
|
+
}
|
|
3894
4002
|
}
|
|
3895
4003
|
for (const node of graph.nodes) {
|
|
3896
4004
|
node.drawingCoord = gridToDrawingCoord(graph, node.gridCoord);
|