Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable CSS-like linear-gradient fills for flowchart nodes #5912

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 88 additions & 3 deletions packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { select } from 'd3';
import type { Selection } from 'd3';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { DiagramStyleClassDef } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
Expand All @@ -8,6 +9,7 @@
import type { LayoutData } from '../../rendering-util/types.js';
import utils from '../../utils.js';
import { getDirection } from './flowDb.js';
import { createLinearGradient } from '../../rendering-util/createGradient.js';

export const getClasses = function (
text: string,
Expand Down Expand Up @@ -54,6 +56,7 @@
data4Layout.diagramId = id;
log.debug('REF1:', data4Layout);
await render(data4Layout, svg);
log.debug('SVG structure:', svg.node().outerHTML);
const padding = data4Layout.config.flowchart?.diagramPadding ?? 8;
utils.insertTitle(
svg,
Expand All @@ -62,12 +65,94 @@
diag.db.getDiagramTitle()
);
setupViewPortForSVG(svg, padding, 'flowchart', conf?.useMaxWidth || false);
log.debug(
'Rendering completed. Starting to process nodes for gradient application and link wrapping...'
);

// If node has a link, wrap it in an anchor SVG object.
// Loop through all nodes
for (const vertex of data4Layout.nodes) {
const node = select(`#${id} [id="${vertex.id}"]`);
log.debug(
`Processing node - ID: "${vertex.id}", domID: "${vertex.domId}", Label: "${vertex.label}"`
);

// Apply gradients to the node's shape if specified in the node's CSS styles
// This has to be done before wrapping the node in an anchor element to avoid selection issues
log.debug(`Attempting to select node using domID with query: #${id} [id="${vertex.domId}"]`);
const nodeSvg = select(`#${id} [id="${vertex.domId}"]`); // selection of the node's SVG element using domId

if (!nodeSvg.empty()) {
log.debug(`Found SVG element for node: ${vertex.domId}`);
// Get the bounding box of the node's shape to extract dimensions
// Assuming shapeElement is a selection of various SVG elements
const shapeElement: Selection<SVGGraphicsElement, unknown, HTMLElement, any> = nodeSvg.select(
'rect, ellipse, circle, polygon, path'
);

if (!shapeElement.empty() && shapeElement.node() !== null) {
log.debug(`Working of node ${vertex.id}->${vertex.domId}`);

// Log all cssCompiledStyles for the node if available
if (vertex.cssCompiledStyles) {
log.debug(`Compiled styles for node ${vertex.id}:`, vertex.cssCompiledStyles);
} else {
log.debug(`No compiled styles found for node ${vertex.id}.`);
}

// Look for all gradient styles, ensuring that nested parentheses due to color functions are handled properly
const linearGradientStyles = vertex.cssCompiledStyles
?.join('')
?.match(/fill\s*:\s*linear-gradient\(([^()]*(\([^()]*\))*[^()]*)+\)/g);

Check failure

Code scanning / CodeQL

Inefficient regular expression High

This part of the regular expression may cause exponential backtracking on strings starting with 'fill:linear-gradient(' and containing many repetitions of '''.

if (linearGradientStyles) {
shapeElement.style('fill', null); // Clear any existing fill
linearGradientStyles.forEach((style, index) => {
log.debug(`Found gradient style ${index + 1} for node ${vertex.id}: "${style}"`);

// Remove the 'fill: linear-gradient()' wrapper to get the gradient definition
const linearGradientStyle = style.replace(/fill\s*:\s*linear-gradient\((.+)\)/, '$1');
const gradientId = `gradient-${vertex.id}-${index}`;

// Create the linear gradient for each occurrence
createLinearGradient(svg, shapeElement, linearGradientStyle, gradientId);

// Clone the shape element to apply each gradient as an overlay
const shapeClone = shapeElement.clone(true);
shapeClone.style('fill', `url(#${gradientId})`);

// Insert the cloned element before the original shape to keep the text/labels on top
const parentNode = shapeElement.node()?.parentNode;
const cloneNode = shapeClone.node();
if (parentNode && cloneNode) {
const nextSibling = shapeElement.node()?.nextSibling;
if (nextSibling) {
parentNode.insertBefore(cloneNode, nextSibling);
} else {
parentNode.appendChild(cloneNode);
}
} else {
log.error(`Parent or clone node not found for shape element: ${vertex.domId}`);
}
// Apply the gradient fill to the node
log.debug(
`Applying gradient ID "${gradientId}" to node: ${vertex.id} with URL: url(#${gradientId})`
);
log.debug(`Underlying SVG element: `, shapeElement.node());
});
} else {
log.debug(`No gradient style found for node ${vertex.id}->${vertex.domId}.`);
}
} else {
log.debug(`Could not find a shape element for node: ${vertex.id}->${vertex.domId}`);
}
continue; // Skip to the next iteration if no node was found
}

// If the node selected by ID has a link, wrap it in an anchor SVG object.
log.debug(`Attempting to select node using ID with query: #${id} [id="${vertex.id}"]`);
// We already selected nodeSvg based on domId; would it work if use it here instead of node?
const node = select(`#${id} [id="${vertex.domId}"]`);
if (!node || !vertex.link) {
continue;
continue; // Skip if the node does not exist or does not have a link property.
}
const link = doc.createElementNS('http://www.w3.org/2000/svg', 'a');
link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.cssClasses);
Expand Down
18 changes: 17 additions & 1 deletion packages/mermaid/src/diagrams/flowchart/parser/flow.jison
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/* lexical grammar */
%lex
%x string
%x linearGradientText
%x md_string
%x acc_title
%x acc_descr
Expand Down Expand Up @@ -133,6 +134,16 @@ that id.

<*>\s*\~\~[\~]+\s* return 'LINK';


/*
Capture linear-gradient(...).
This includes `linear-gradient(-...)` which otherwise conflicts with `(-` in ellipseText.
*/
<*>"linear-gradient"[\s]*\(\s* { this.pushState("linearGradientText"); return 'LINEAR_GRADIENT_START'; }
<linearGradientText>([^()]|(\([^()]*\)))+ { return 'LINEAR_GRADIENT_CONTENT'; } // Handles text, commas, and nested parentheses
<linearGradientText>\) { this.popState(); return 'LINEAR_GRADIENT_END'; }


<ellipseText>[-/\)][\)] { this.popState(); return '-)'; }
<ellipseText>[^\(\)\[\]\{\}]|-\!\)+ return "TEXT"
<*>"(-" { this.pushState("ellipseText"); return '(-'; }
Expand Down Expand Up @@ -535,7 +546,12 @@ style: styleComponent
{$$ = $style + $styleComponent;}
;

styleComponent: NUM | NODE_STRING| COLON | UNIT | SPACE | BRKT | STYLE | PCT ;
styleComponent: NUM | NODE_STRING | COLON | UNIT | SPACE | BRKT | STYLE | PS | TEXT | PE | LINGRAD;

LINGRAD
: LINEAR_GRADIENT_START LINEAR_GRADIENT_CONTENT LINEAR_GRADIENT_END
{ $$ = 'linear-gradient(' + $2 + ')'; }
;

/* Token lists */
idStringToken : NUM | NODE_STRING | DOWN | MINUS | DEFAULT | COMMA | COLON | AMP | BRKT | MULT | UNICODE_TEXT;
Expand Down
Loading
Loading