Merge develop into release/11.5.0

This commit is contained in:
Ashish Jain
2025-03-12 13:40:16 +01:00
4 changed files with 93 additions and 29 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': minor
---
The arrowhead color should match the color of the edge. Creates a unique clone of the arrow marker with the appropriate color.

View File

@@ -917,4 +917,21 @@ graph TD
}
);
});
it('#6369: edge color should affect arrow head', () => {
imgSnapshotTest(
`
flowchart LR
A --> B
A --> C
C --> D
linkStyle 0 stroke:#D50000
linkStyle 2 stroke:#D50000
`,
{
flowchart: { htmlLabels: true },
securityLevel: 'loose',
}
);
});
});

View File

@@ -15,32 +15,33 @@ export const addEdgeMarkers = (
edge: Pick<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>,
url: string,
id: string,
diagramType: string
diagramType: string,
strokeColor?: string
) => {
if (edge.arrowTypeStart) {
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType);
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType, strokeColor);
}
if (edge.arrowTypeEnd) {
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType);
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType, strokeColor);
}
};
const arrowTypesMap = {
arrow_cross: 'cross',
arrow_point: 'point',
arrow_barb: 'barb',
arrow_circle: 'circle',
aggregation: 'aggregation',
extension: 'extension',
composition: 'composition',
dependency: 'dependency',
lollipop: 'lollipop',
only_one: 'onlyOne',
zero_or_one: 'zeroOrOne',
one_or_more: 'oneOrMore',
zero_or_more: 'zeroOrMore',
requirement_arrow: 'requirement_arrow',
requirement_contains: 'requirement_contains',
arrow_cross: { type: 'cross', fill: false },
arrow_point: { type: 'point', fill: true },
arrow_barb: { type: 'barb', fill: true },
arrow_circle: { type: 'circle', fill: false },
aggregation: { type: 'aggregation', fill: false },
extension: { type: 'extension', fill: false },
composition: { type: 'composition', fill: true },
dependency: { type: 'dependency', fill: true },
lollipop: { type: 'lollipop', fill: false },
only_one: { type: 'onlyOne', fill: false },
zero_or_one: { type: 'zeroOrOne', fill: false },
one_or_more: { type: 'oneOrMore', fill: false },
zero_or_more: { type: 'zeroOrMore', fill: false },
requirement_arrow: { type: 'requirement_arrow', fill: false },
requirement_contains: { type: 'requirement_contains', fill: false },
} as const;
const addEdgeMarker = (
@@ -49,15 +50,55 @@ const addEdgeMarker = (
arrowType: string,
url: string,
id: string,
diagramType: string
diagramType: string,
strokeColor?: string
) => {
const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
const arrowTypeInfo = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
if (!endMarkerType) {
if (!arrowTypeInfo) {
log.warn(`Unknown arrow type: ${arrowType}`);
return; // unknown arrow type, ignore
}
const endMarkerType = arrowTypeInfo.type;
const suffix = position === 'start' ? 'Start' : 'End';
svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`);
const originalMarkerId = `${id}_${diagramType}-${endMarkerType}${suffix}`;
// If stroke color is specified and non-empty, create or use a colored variant of the marker
if (strokeColor && strokeColor.trim() !== '') {
// Create a sanitized color value for use in IDs
const colorId = strokeColor.replace(/[^\dA-Za-z]/g, '_');
const coloredMarkerId = `${originalMarkerId}_${colorId}`;
// Check if the colored marker already exists
if (!document.getElementById(coloredMarkerId)) {
// Get the original marker
const originalMarker = document.getElementById(originalMarkerId);
if (originalMarker) {
// Clone the marker and create colored version
const coloredMarker = originalMarker.cloneNode(true) as Element;
coloredMarker.id = coloredMarkerId;
// Apply colors to the paths inside the marker
const paths = coloredMarker.querySelectorAll('path, circle, line');
paths.forEach((path) => {
path.setAttribute('stroke', strokeColor);
// Apply fill only to markers that should be filled
if (arrowTypeInfo.fill) {
path.setAttribute('fill', strokeColor);
}
});
// Add the new colored marker to the defs section
originalMarker.parentNode?.appendChild(coloredMarker);
}
}
// Use the colored marker
svgPath.attr(`marker-${position}`, `url(${url}#${coloredMarkerId})`);
} else {
// Always use the original marker for unstyled edges
svgPath.attr(`marker-${position}`, `url(${url}#${originalMarkerId})`);
}
};

View File

@@ -521,6 +521,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
let svgPath;
let linePath = lineFunction(lineData);
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
let strokeColor = edgeStyles.find((style) => style.startsWith('stroke:'));
if (edge.look === 'handDrawn') {
const rc = rough.svg(elem);
@@ -551,18 +552,18 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
if (edge.animation) {
animationClass = ' edge-animation-' + edge.animation;
}
const pathStyle = stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles;
svgPath = elem
.append('path')
.attr('d', linePath)
.attr('id', edge.id)
.attr(
'class',
' ' +
strokeClasses +
(edge.classes ? ' ' + edge.classes : '') +
(animationClass ? animationClass : '')
' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '') + (animationClass ?? '')
)
.attr('style', stylesFromClasses ? stylesFromClasses + ';' + styles + ';' : styles);
.attr('style', pathStyle);
strokeColor = pathStyle.match(/stroke:([^;]+)/)?.[1];
}
// DEBUG code, DO NOT REMOVE
@@ -599,7 +600,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
log.info('arrowTypeStart', edge.arrowTypeStart);
log.info('arrowTypeEnd', edge.arrowTypeEnd);
addEdgeMarkers(svgPath, edge, url, id, diagramType);
addEdgeMarkers(svgPath, edge, url, id, diagramType, strokeColor);
let paths = {};
if (pointsHasChanged) {