Merge branch 'develop' of https://github.com/mermaid-js/mermaid into renovate/patch-all-patch

This commit is contained in:
shubhamparikh2704
2025-09-15 14:32:02 +05:30
23 changed files with 4806 additions and 5697 deletions

View File

@@ -0,0 +1,48 @@
import { describe } from 'vitest';
import { draw } from './architectureRenderer.js';
import { Diagram } from '../../Diagram.js';
import { addDetector } from '../../diagram-api/detectType.js';
import architectureDetector from './architectureDetector.js';
import { ensureNodeFromSelector, jsdomIt } from '../../tests/util.js';
const { id, detector, loader } = architectureDetector;
addDetector(id, detector, loader); // Add architecture schemas to Mermaid
describe('architecture diagram SVGs', () => {
jsdomIt('should add ids', async () => {
const svgNode = await drawDiagram(`
architecture-beta
group api(cloud)[API]
service db(database)[Database] in api
service disk1(disk)[Storage] in api
service disk2(disk)[Storage] in api
service server(server)[Server] in api
db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
`);
const nodesForGroup = svgNode.querySelectorAll(`#group-api`);
expect(nodesForGroup.length).toBe(1);
const serviceIds = [...svgNode.querySelectorAll(`[id^=service-]`)].map(({ id }) => id).sort();
expect(serviceIds).toStrictEqual([
'service-db',
'service-disk1',
'service-disk2',
'service-server',
]);
const edgeIds = [...svgNode.querySelectorAll(`.edge[id^=L_]`)].map(({ id }) => id).sort();
expect(edgeIds).toStrictEqual(['L_db_server_0', 'L_disk1_server_0', 'L_disk2_db_0']);
});
});
async function drawDiagram(diagramText: string): Promise<Element> {
const diagram = await Diagram.fromText(diagramText, {});
await draw('NOT_USED', 'svg', '1.0.0', diagram);
return ensureNodeFromSelector('#svg');
}

View File

@@ -20,6 +20,7 @@ import {
type ArchitectureJunction,
type ArchitectureService,
} from './architectureTypes.js';
import { getEdgeId } from '../../utils.js';
export const drawEdges = async function (
edgesEl: D3Element,
@@ -91,7 +92,8 @@ export const drawEdges = async function (
g.insert('path')
.attr('d', `M ${startX},${startY} L ${midX},${midY} L${endX},${endY} `)
.attr('class', 'edge');
.attr('class', 'edge')
.attr('id', getEdgeId(source, target, { prefix: 'L' }));
if (sourceArrow) {
const xShift = isArchitectureDirectionX(sourceDir)
@@ -206,8 +208,9 @@ export const drawGroups = async function (
if (data.type === 'group') {
const { h, w, x1, y1 } = node.boundingBox();
groupsEl
.append('rect')
const groupsNode = groupsEl.append('rect');
groupsNode
.attr('id', `group-${data.id}`)
.attr('x', x1 + halfIconSize)
.attr('y', y1 + halfIconSize)
.attr('width', w)
@@ -262,6 +265,7 @@ export const drawGroups = async function (
')'
);
}
db.setElementForId(data.id, groupsNode);
}
})
);
@@ -342,9 +346,9 @@ export const drawServices = async function (
);
}
serviceElem.attr('class', 'architecture-service');
serviceElem.attr('id', `service-${service.id}`).attr('class', 'architecture-service');
const { width, height } = serviceElem._groups[0][0].getBBox();
const { width, height } = serviceElem.node().getBBox();
service.width = width;
service.height = height;
db.setElementForId(service.id, serviceElem);

View File

@@ -37,6 +37,7 @@ export class MindmapDB {
private nodes: MindmapNode[] = [];
private count = 0;
private elements: Record<number, D3Element> = {};
private baseLevel?: number;
public readonly nodeType: typeof nodeType;
constructor() {
@@ -54,6 +55,7 @@ export class MindmapDB {
this.nodes = [];
this.count = 0;
this.elements = {};
this.baseLevel = undefined;
}
public getParent(level: number): MindmapNode | null {
@@ -72,6 +74,17 @@ export class MindmapDB {
public addNode(level: number, id: string, descr: string, type: number): void {
log.info('addNode', level, id, descr, type);
let isRoot = false;
if (this.nodes.length === 0) {
this.baseLevel = level;
level = 0;
isRoot = true;
} else if (this.baseLevel !== undefined) {
level = level - this.baseLevel;
isRoot = false;
}
const conf = getConfig();
let padding = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
@@ -92,6 +105,7 @@ export class MindmapDB {
children: [],
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
padding,
isRoot,
};
const parent = this.getParent(level);
@@ -99,7 +113,7 @@ export class MindmapDB {
parent.children.push(node);
this.nodes.push(node);
} else {
if (this.nodes.length === 0) {
if (isRoot) {
this.nodes.push(node);
} else {
throw new Error(
@@ -204,8 +218,7 @@ export class MindmapDB {
// Build CSS classes for the node
const cssClasses = ['mindmap-node'];
// Add section-specific classes
if (node.level === 0) {
if (node.isRoot === true) {
// Root node gets special classes
cssClasses.push('section-root', 'section--1');
} else if (node.section !== undefined) {

View File

@@ -15,6 +15,7 @@ export interface MindmapNode {
icon?: string;
x?: number;
y?: number;
isRoot?: boolean;
}
export type FilledMindMapNode = RequiredDeep<MindmapNode>;

View File

@@ -1,7 +1,13 @@
import mermaid, { type MermaidConfig } from 'mermaid';
import zenuml from '../../../../../mermaid-zenuml/dist/mermaid-zenuml.core.mjs';
import tidyTreeLayout from '../../../../../mermaid-layout-tidy-tree/dist/mermaid-layout-tidy-tree.core.mjs';
import layouts from '../../../../../mermaid-layout-elk/dist/mermaid-layout-elk.core.mjs';
const init = mermaid.registerExternalDiagrams([zenuml]);
const init = Promise.all([
mermaid.registerExternalDiagrams([zenuml]),
mermaid.registerLayoutLoaders(layouts),
mermaid.registerLayoutLoaders(tidyTreeLayout),
]);
mermaid.registerIconPacks([
{
name: 'logos',

View File

@@ -32,10 +32,10 @@
"https-localhost": "^4.7.1",
"pathe": "^2.0.3",
"unocss": "^66.4.2",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.1.1",
"vite-plugin-pwa": "^1.0.3",
"vitepress": "1.6.4",
"unplugin-vue-components": "^28.4.0",
"vite": "^7.0.0",
"vite-plugin-pwa": "^1.0.0",
"vitepress": "1.6.3",
"workbox-window": "^7.3.0"
}
}

View File

@@ -13,6 +13,10 @@ const virtualModuleId = 'virtual:mermaid-config';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
export default defineConfig({
build: {
// Vite v7 changes the default target and drops old browser support
target: 'modules',
},
optimizeDeps: {
// vitepress is aliased with replacement `join(DIST_CLIENT_PATH, '/index')`
// This needs to be excluded from optimization

View File

@@ -4,6 +4,9 @@ import { internalHelpers } from '../internals.js';
import { log } from '../logger.js';
import type { LayoutData } from './types.js';
// console.log('MUST be removed, this only for keeping dev server working');
// import tmp from './layout-algorithms/dagre/index.js';
export interface RenderOptions {
algorithm?: string;
}

View File

@@ -1,9 +1,13 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { evaluate, getUrl } from '../../diagrams/common/common.js';
import { evaluate } from '../../diagrams/common/common.js';
import { log } from '../../logger.js';
import { createText } from '../createText.js';
import utils from '../../utils.js';
import { getLineFunctionsWithOffset } from '../../utils/lineWithOffset.js';
import {
getLineFunctionsWithOffset,
markerOffsets,
markerOffsets2,
} from '../../utils/lineWithOffset.js';
import { getSubGraphTitleMargins } from '../../utils/subGraphTitleMargins.js';
import {
@@ -27,8 +31,8 @@ import createLabel from './createLabel.js';
import { addEdgeMarkers } from './edgeMarker.ts';
import { isLabelStyle, styles2String } from './shapes/handDrawnShapeStyles.js';
const edgeLabels = new Map();
const terminalLabels = new Map();
export const edgeLabels = new Map();
export const terminalLabels = new Map();
export const clear = () => {
edgeLabels.clear();
@@ -57,7 +61,7 @@ export const insertEdgeLabel = async (elem, edge) => {
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
// Create inner g, label, this will be positioned now for centering the text
const label = edgeLabel.insert('g').attr('class', 'label');
const label = edgeLabel.insert('g').attr('class', 'label').attr('data-id', edge.id);
label.node().appendChild(labelElement);
// Center the label
@@ -440,7 +444,33 @@ const fixCorners = function (lineData) {
}
return newLineData;
};
export const insertEdge = function (elem, edge, clusterDb, diagramType, startNode, endNode, id) {
const generateDashArray = (len, oValueS, oValueE) => {
const middleLength = len - oValueS - oValueE;
const dashLength = 2; // Length of each dash
const gapLength = 2; // Length of each gap
const dashGapPairLength = dashLength + gapLength;
// Calculate number of complete dash-gap pairs that can fit
const numberOfPairs = Math.floor(middleLength / dashGapPairLength);
// Generate the middle pattern array
const middlePattern = Array(numberOfPairs).fill(`${dashLength} ${gapLength}`).join(' ');
// Combine all parts
const dashArray = `0 ${oValueS} ${middlePattern} ${oValueE}`;
return dashArray;
};
export const insertEdge = function (
elem,
edge,
clusterDb,
diagramType,
startNode,
endNode,
id,
skipIntersect = false
) {
const { handDrawnSeed } = getConfig();
let points = edge.points;
let pointsHasChanged = false;
@@ -454,11 +484,12 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
edgeClassStyles.push(edge.cssCompiledStyles[key]);
}
if (head.intersect && tail.intersect) {
log.debug('UIO intersect check', edge.points, head.x, tail.x);
if (head.intersect && tail.intersect && !skipIntersect) {
points = points.slice(1, edge.points.length - 1);
points.unshift(tail.intersect(points[0]));
log.debug(
'Last point APA12',
'Last point UIO',
edge.start,
'-->',
edge.end,
@@ -468,6 +499,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
);
points.push(head.intersect(points[points.length - 1]));
}
const pointsStr = btoa(JSON.stringify(points));
if (edge.toCluster) {
log.info('to cluster abc88', clusterDb.get(edge.toCluster));
points = cutPathAtIntersect(edge.points, clusterDb.get(edge.toCluster).node);
@@ -531,6 +563,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
curve = curveBasis;
}
// if (edge.curve) {
// curve = edge.curve;
// }
const { x, y } = getLineFunctionsWithOffset(edge);
const lineFunction = line().x(x).y(y).curve(curve);
@@ -562,10 +598,14 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
strokeClasses += ' edge-pattern-solid';
}
let svgPath;
let linePath = lineFunction(lineData);
const edgeStyles = Array.isArray(edge.style) ? edge.style : edge.style ? [edge.style] : [];
let linePath =
edge.curve === 'rounded'
? generateRoundedPath(applyMarkerOffsetsToPoints(lineData, edge), 5)
: lineFunction(lineData);
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
let animatedEdge = false;
if (edge.look === 'handDrawn') {
const rc = rough.svg(elem);
Object.assign([], lineData);
@@ -596,7 +636,10 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
animationClass = ' edge-animation-' + edge.animation;
}
const pathStyle = stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles;
const pathStyle =
(stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles) +
';' +
(edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : '');
svgPath = elem
.append('path')
.attr('d', linePath)
@@ -606,11 +649,39 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
)
.attr('style', pathStyle);
//eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
strokeColor = pathStyle.match(/stroke:([^;]+)/)?.[1];
// Possible fix to remove eslint-disable-next-line
//strokeColor = /stroke:([^;]+)/.exec(pathStyle)?.[1];
animatedEdge =
edge.animate === true || !!edge.animation || stylesFromClasses.includes('animation');
const pathNode = svgPath.node();
const len = typeof pathNode.getTotalLength === 'function' ? pathNode.getTotalLength() : 0;
const oValueS = markerOffsets2[edge.arrowTypeStart] || 0;
const oValueE = markerOffsets2[edge.arrowTypeEnd] || 0;
if (edge.look === 'neo' && !animatedEdge) {
const dashArray =
edge.pattern === 'dotted' || edge.pattern === 'dashed'
? generateDashArray(len, oValueS, oValueE)
: `0 ${oValueS} ${len - oValueS - oValueE} ${oValueE}`;
// No offset needed because we already start with a zero-length dash that effectively sets us up for a gap at the start.
const mOffset = `stroke-dasharray: ${dashArray}; stroke-dashoffset: 0;`;
svgPath.attr('style', mOffset + svgPath.attr('style'));
}
}
// DEBUG code, DO NOT REMOVE
// adds a red circle at each edge coordinate
// MC Special
svgPath.attr('data-edge', true);
svgPath.attr('data-et', 'edge');
svgPath.attr('data-id', edge.id);
svgPath.attr('data-points', pointsStr);
// DEBUG code, adds a red circle at each edge coordinate
// cornerPoints.forEach((point) => {
// elem
// .append('circle')
@@ -620,19 +691,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
// .attr('cx', point.x)
// .attr('cy', point.y);
// });
// lineData.forEach((point) => {
// elem
// .append('circle')
// .style('stroke', 'red')
// .style('fill', 'red')
// .attr('r', 1)
// .attr('cx', point.x)
// .attr('cy', point.y);
// });
if (edge.showPoints) {
lineData.forEach((point) => {
elem
.append('circle')
.style('stroke', 'red')
.style('fill', 'red')
.attr('r', 1)
.attr('cx', point.x)
.attr('cy', point.y);
});
}
let url = '';
if (getConfig().flowchart.arrowMarkerAbsolute || getConfig().state.arrowMarkerAbsolute) {
url = getUrl(true);
url =
window.location.protocol +
'//' +
window.location.host +
window.location.pathname +
window.location.search;
url = url.replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
log.info('arrowTypeStart', edge.arrowTypeStart);
log.info('arrowTypeEnd', edge.arrowTypeEnd);
@@ -651,3 +730,134 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
paths.originalPath = edge.points;
return paths;
};
/**
* Generates SVG path data with rounded corners from an array of points.
* @param {Array} points - Array of points in the format [{x: Number, y: Number}, ...]
* @param {Number} radius - The radius of the rounded corners
* @returns {String} - SVG path data string
*/
function generateRoundedPath(points, radius) {
if (points.length < 2) {
return '';
}
let path = '';
const size = points.length;
const epsilon = 1e-5;
for (let i = 0; i < size; i++) {
const currPoint = points[i];
const prevPoint = points[i - 1];
const nextPoint = points[i + 1];
if (i === 0) {
// Move to the first point
path += `M${currPoint.x},${currPoint.y}`;
} else if (i === size - 1) {
// Last point, draw a straight line to the final point
path += `L${currPoint.x},${currPoint.y}`;
} else {
// Calculate vectors for incoming and outgoing segments
const dx1 = currPoint.x - prevPoint.x;
const dy1 = currPoint.y - prevPoint.y;
const dx2 = nextPoint.x - currPoint.x;
const dy2 = nextPoint.y - currPoint.y;
const len1 = Math.hypot(dx1, dy1);
const len2 = Math.hypot(dx2, dy2);
// Prevent division by zero
if (len1 < epsilon || len2 < epsilon) {
path += `L${currPoint.x},${currPoint.y}`;
continue;
}
// Normalize the vectors
const nx1 = dx1 / len1;
const ny1 = dy1 / len1;
const nx2 = dx2 / len2;
const ny2 = dy2 / len2;
// Calculate the angle between the vectors
const dot = nx1 * nx2 + ny1 * ny2;
// Clamp the dot product to avoid numerical issues with acos
const clampedDot = Math.max(-1, Math.min(1, dot));
const angle = Math.acos(clampedDot);
// Skip rounding if the angle is too small or too close to 180 degrees
if (angle < epsilon || Math.abs(Math.PI - angle) < epsilon) {
path += `L${currPoint.x},${currPoint.y}`;
continue;
}
// Calculate the distance to offset the control point
const cutLen = Math.min(radius / Math.sin(angle / 2), len1 / 2, len2 / 2);
// Calculate the start and end points of the curve
const startX = currPoint.x - nx1 * cutLen;
const startY = currPoint.y - ny1 * cutLen;
const endX = currPoint.x + nx2 * cutLen;
const endY = currPoint.y + ny2 * cutLen;
// Draw the line to the start of the curve
path += `L${startX},${startY}`;
// Draw the quadratic Bezier curve
path += `Q${currPoint.x},${currPoint.y} ${endX},${endY}`;
}
}
return path;
}
// Helper function to calculate delta and angle between two points
function calculateDeltaAndAngle(point1, point2) {
if (!point1 || !point2) {
return { angle: 0, deltaX: 0, deltaY: 0 };
}
const deltaX = point2.x - point1.x;
const deltaY = point2.y - point1.y;
const angle = Math.atan2(deltaY, deltaX);
return { angle, deltaX, deltaY };
}
// Function to adjust the first and last points of the points array
function applyMarkerOffsetsToPoints(points, edge) {
// Copy the points array to avoid mutating the original data
const newPoints = points.map((point) => ({ ...point }));
// Handle the first point (start of the edge)
if (points.length >= 2 && markerOffsets[edge.arrowTypeStart]) {
const offsetValue = markerOffsets[edge.arrowTypeStart];
const point1 = points[0];
const point2 = points[1];
const { angle } = calculateDeltaAndAngle(point1, point2);
const offsetX = offsetValue * Math.cos(angle);
const offsetY = offsetValue * Math.sin(angle);
newPoints[0].x = point1.x + offsetX;
newPoints[0].y = point1.y + offsetY;
}
// Handle the last point (end of the edge)
const n = points.length;
if (n >= 2 && markerOffsets[edge.arrowTypeEnd]) {
const offsetValue = markerOffsets[edge.arrowTypeEnd];
const point1 = points[n - 1];
const point2 = points[n - 2];
const { angle } = calculateDeltaAndAngle(point2, point1);
const offsetX = offsetValue * Math.cos(angle);
const offsetY = offsetValue * Math.sin(angle);
newPoints[n - 1].x = point1.x - offsetX;
newPoints[n - 1].y = point1.y - offsetY;
}
return newPoints;
}

View File

@@ -4,12 +4,22 @@ import type { EdgeData, Point } from '../types.js';
// under any transparent markers.
// The offsets are calculated from the markers' dimensions.
export const markerOffsets = {
aggregation: 18,
extension: 18,
composition: 18,
aggregation: 17.25,
extension: 17.25,
composition: 17.25,
dependency: 6,
lollipop: 13.5,
arrow_point: 4,
//arrow_cross: 24,
} as const;
// We need to draw the lines a bit shorter to avoid drawing
// under any transparent markers.
// The offsets are calculated from the markers' dimensions.
export const markerOffsets2 = {
arrow_point: 9,
arrow_cross: 12.5,
arrow_circle: 12.5,
} as const;
/**
@@ -104,6 +114,7 @@ export const getLineFunctionsWithOffset = (
adjustment *= DIRECTION === 'right' ? -1 : 1;
offset += adjustment;
}
return pointTransformer(d).x + offset;
},
y: function (