diff --git a/.changeset/vast-nails-stay.md b/.changeset/vast-nails-stay.md new file mode 100644 index 000000000..de2059b32 --- /dev/null +++ b/.changeset/vast-nails-stay.md @@ -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. diff --git a/cypress/integration/rendering/flowchart.spec.js b/cypress/integration/rendering/flowchart.spec.js index d3a83ae5f..7b986cd2f 100644 --- a/cypress/integration/rendering/flowchart.spec.js +++ b/cypress/integration/rendering/flowchart.spec.js @@ -895,7 +895,7 @@ graph TD imgSnapshotTest( ` graph TD - classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff + classDef default fill:#a34,stroke:#000,stroke-width:4px,color:#fff hello --> default `, { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } @@ -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', + } + ); + }); }); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts index 6c75a9828..70d0e0ec8 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/edgeMarker.ts @@ -15,32 +15,33 @@ export const addEdgeMarkers = ( edge: Pick, 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})`); + } }; diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 1bee271e0..07d9ab096 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -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) {