Skip to content

Refactor fontawesome icon usage. #6282

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

Merged
merged 23 commits into from
Apr 20, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
302ba72
embed free font awesome svg instead of i tag. pro icons will still wo…
saurabhg772244 Feb 14, 2025
33e5694
update styles so that proper fill is applied to icons
saurabhg772244 Feb 14, 2025
d63d3bf
added changeset
saurabhg772244 Feb 14, 2025
a81c318
removed font awesome packages and used registered icons instead
saurabhg772244 Feb 18, 2025
8857e77
Merge branch 'develop' of https://github.com/mermaid-js/mermaid into …
saurabhg772244 Feb 19, 2025
16573d9
Updated docs, added visual regression test
saurabhg772244 Feb 21, 2025
6a81a90
Merge branch 'develop' of g.yxqyang.asia-mermaid:mermaid-js/mermaid into …
saurabhg772244 Feb 21, 2025
a89e001
Merge branch 'saurabh/refactor-fontawesome-icon-usage' of g.yxqyang.asia-…
saurabhg772244 Feb 21, 2025
83a6e69
fix lock file
saurabhg772244 Feb 21, 2025
2fb7bc2
fix pnpm lock and use promise.all to speed up icon replacement
saurabhg772244 Feb 21, 2025
e045e0b
Code refactor and updated docs
saurabhg772244 Feb 25, 2025
b0c1460
Convert Font Awesome loading from HTML to JS
saurabhg772244 Feb 26, 2025
1be1e34
Update packages/mermaid/src/docs/syntax/flowchart.md
saurabhg772244 Feb 26, 2025
050fbd9
Update packages/mermaid/src/docs/syntax/flowchart.md
saurabhg772244 Feb 26, 2025
06877dc
Update cypress/platform/viewer.js
saurabhg772244 Feb 26, 2025
f60ecfc
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 26, 2025
a31adc6
Updated docs and e2e test case
saurabhg772244 Feb 28, 2025
24f23ef
Merge branch 'develop' into saurabh/refactor-fontawesome-icon-usage
sidharthv96 Apr 15, 2025
53e89d9
Merge branch 'develop' into saurabh/refactor-fontawesome-icon-usage
sidharthv96 Apr 18, 2025
b0b76ef
temp: Remove tests to check if cypress passes
sidharthv96 Apr 19, 2025
5ea7088
test: fix flowchart width in test
sidharthv96 Apr 19, 2025
867484a
Revert "temp: Remove tests to check if cypress passes"
sidharthv96 Apr 19, 2025
4077088
test: Tweak flowchart tests
sidharthv96 Apr 19, 2025
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
5 changes: 5 additions & 0 deletions .changeset/proud-seahorses-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mermaid': patch
---

FontAwesome icons can now be embedded as SVGs in flowcharts if they are registered via `mermaid.registerIconPacks`.
32 changes: 32 additions & 0 deletions cypress/integration/rendering/flowchart-icon.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { imgSnapshotTest } from '../../helpers/util.ts';

const themes = ['default', 'forest', 'dark', 'base', 'neutral'];

themes.forEach((theme, index) => {
describe('Flowchart Icon', () => {
it(`${index + 1}-icon: verify if icons are working from fontawesome library ${theme} theme`, () => {
imgSnapshotTest(
`flowchart TD
A("fab:fa-twitter Twitter") --> B("fab:fa-facebook Facebook")
B --> C("fa:fa-coffee Coffee")
C --> D("fa:fa-car Car")
D --> E("fab:fa-github GitHub")
`,
{ theme }
);
});
});
});

themes.forEach((theme, index) => {
describe('Flowchart Icon', () => {
it(`${index + 1}-icon: verify if registered icons are working on ${theme} theme`, () => {
imgSnapshotTest(
`flowchart TD
A("fa:fa-bell Bell")
`,
{ theme }
);
});
});
});
3 changes: 3 additions & 0 deletions cypress/platform/e2e.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
.exClass {
fill: greenyellow !important;
}
.label-icon {
border: none;
}
</style>
</head>
<body></body>
Expand Down
22 changes: 21 additions & 1 deletion cypress/platform/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,32 @@ function markRendered() {
}
}

function loadFontAwesomeCSS() {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css';

document.head.appendChild(link);

return new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = () => reject(new Error('Failed to load FontAwesome'));
});
}

/**
* ##contentLoaded Callback function that is called when page is loaded. This functions fetches
* configuration for mermaid rendering and calls init for rendering the mermaid diagrams on the
* page.
*/
const contentLoaded = async function () {
try {
await loadFontAwesomeCSS();
await Promise.all(Array.from(document.fonts, (font) => font.load()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue(blocking):

This change now causes the following E2E tests to fail

it('7: should render a flowchart when useMaxWidth is true (default)', () => {
renderGraph(
`flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]
`,
{ flowchart: { useMaxWidth: true } }
);
cy.get('svg').should((svg) => {
expect(svg).to.have.attr('width', '100%');
// expect(svg).to.have.attr('height');
// use within because the absolute value can be slightly different depending on the environment ±5%
// const height = parseFloat(svg.attr('height'));
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
expect(maxWidthValue).to.be.within(417 * 0.95, 417 * 1.05);
});
});
it('8: should render a flowchart when useMaxWidth is false', () => {
renderGraph(
`flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]
`,
{ flowchart: { useMaxWidth: false } }
);
cy.get('svg').should((svg) => {
// const height = parseFloat(svg.attr('height'));
const width = parseFloat(svg.attr('width'));
// use within because the absolute value can be slightly different depending on the environment ±5%
// expect(height).to.be.within(446 * 0.95, 446 * 1.05);
expect(width).to.be.within(417 * 0.95, 417 * 1.05);
expect(svg).to.not.have.attr('style');
});
});

We could remove the fa:fa-car icon? Or update the widths to the new value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change in the images seems to be the /rendering prefix in the test name. Please fix that as well.

image image

} catch (error) {
console.error('Error loading fonts', error);
}

let pos = document.location.href.indexOf('?graph=');
if (pos > 0) {
pos = pos + 7;
Expand Down Expand Up @@ -51,7 +71,7 @@ const contentLoaded = async function () {
mermaid.registerLayoutLoaders(layouts);
mermaid.initialize(graphObj.mermaid);
const staticBellIconPack = {
prefix: 'fa6-regular',
prefix: 'fa',
icons: {
bell: {
body: '<path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32v19.2C119 66 64 130.6 64 208v25.4c0 45.4-15.5 89.5-43.8 124.9L5.3 377c-5.8 7.2-6.9 17.1-2.9 25.4S14.8 416 24 416h400c9.2 0 17.6-5.3 21.6-13.6s2.9-18.2-2.9-25.4l-14.9-18.6c-28.3-35.5-43.8-79.6-43.8-125V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32m0 96c61.9 0 112 50.1 112 112v25.4c0 47.9 13.9 94.6 39.7 134.6H72.3c25.8-40 39.7-86.7 39.7-134.6V208c0-61.9 50.1-112 112-112m64 352H160c0 17 6.7 33.3 18.7 45.3S207 512 224 512s33.3-6.7 45.3-18.7S288 465 288 448"/>',
Expand Down
11 changes: 11 additions & 0 deletions docs/syntax/flowchart.md
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,17 @@ flowchart TD
B-->E(A fa:fa-camera-retro perhaps?)
```

There are two ways to display these FontAwesome icons:

### Register FontAwesome icon packs (v11.4.2+)

You can register your own FontAwesome icon pack, to register follow the instructions provided [here](../config/icons.md).

> **Note**
> Note that it will fall back to FontAwesome CSS if FontAwesome packs are not registered.

### Register FontAwesome CSS

Mermaid supports Font Awesome if the CSS is included on the website.
Mermaid does not have any restriction on the version of Font Awesome that can be used.

Expand Down
18 changes: 10 additions & 8 deletions packages/mermaid/src/dagre-wrapper/clusters.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getConfig } from '../diagram-api/diagramAPI.js';
import { evaluate } from '../diagrams/common/common.js';
import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';

const rect = (parent, node) => {
const rect = async (parent, node) => {
log.info('Creating subgraph rect for ', node.id, node);
const siteConfig = getConfig();

Expand All @@ -31,7 +31,9 @@ const rect = (parent, node) => {
const text =
node.labelType === 'markdown'
? createText(label, node.labelText, { style: node.labelStyle, useHtmlLabels }, siteConfig)
: label.node().appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
: label
.node()
.appendChild(await createLabel(node.labelText, node.labelStyle, undefined, true));

// Get the size of the label
let bbox = text.getBBox();
Expand Down Expand Up @@ -129,7 +131,7 @@ const noteGroup = (parent, node) => {

return shapeSvg;
};
const roundedWithTitle = (parent, node) => {
const roundedWithTitle = async (parent, node) => {
const siteConfig = getConfig();

// Add outer g element
Expand All @@ -144,7 +146,7 @@ const roundedWithTitle = (parent, node) => {

const text = label
.node()
.appendChild(createLabel(node.labelText, node.labelStyle, undefined, true));
.appendChild(await createLabel(node.labelText, node.labelStyle, undefined, true));

// Get the size of the label
let bbox = text.getBBox();
Expand Down Expand Up @@ -236,13 +238,13 @@ const shapes = { rect, roundedWithTitle, noteGroup, divider };

let clusterElems = {};

export const insertCluster = (elem, node) => {
export const insertCluster = async (elem, node) => {
log.trace('Inserting cluster');
const shape = node.shape || 'rect';
clusterElems[node.id] = shapes[shape](elem, node);
clusterElems[node.id] = await shapes[shape](elem, node);
};
export const getClusterTitleWidth = (elem, node) => {
const label = createLabel(node.labelText, node.labelStyle, undefined, true);
export const getClusterTitleWidth = async (elem, node) => {
const label = await createLabel(node.labelText, node.labelStyle, undefined, true);
elem.node().appendChild(label);
const width = label.getBBox().width;
elem.node().removeChild(label);
Expand Down
5 changes: 3 additions & 2 deletions packages/mermaid/src/dagre-wrapper/createLabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function addHtmlLabel(node) {
* @param isNode
* @deprecated svg-util/createText instead
*/
const createLabel = (_vertexText, style, isTitle, isNode) => {
const createLabel = async (_vertexText, style, isTitle, isNode) => {
let vertexText = _vertexText || '';
if (typeof vertexText === 'object') {
vertexText = vertexText[0];
Expand All @@ -53,9 +53,10 @@ const createLabel = (_vertexText, style, isTitle, isNode) => {
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
vertexText = vertexText.replace(/\\n|\n/g, '<br />');
log.debug('vertexText' + vertexText);
const label = await replaceIconSubstring(decodeEntities(vertexText));
const node = {
isNode,
label: replaceIconSubstring(decodeEntities(vertexText)),
label,
labelStyle: style.replace('fill:', 'color:'),
};
let vertexNode = addHtmlLabel(node);
Expand Down
12 changes: 6 additions & 6 deletions packages/mermaid/src/dagre-wrapper/edges.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const clear = () => {
terminalLabels = {};
};

export const insertEdgeLabel = (elem, edge) => {
export const insertEdgeLabel = async (elem, edge) => {
const config = getConfig();
const useHtmlLabels = evaluate(config.flowchart.htmlLabels);
// Create the actual text element
Expand All @@ -33,7 +33,7 @@ export const insertEdgeLabel = (elem, edge) => {
},
config
)
: createLabel(edge.label, edge.labelStyle);
: await createLabel(edge.label, edge.labelStyle);

// Create outer g, edgeLabel, this will be positioned after graph layout
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
Expand Down Expand Up @@ -63,7 +63,7 @@ export const insertEdgeLabel = (elem, edge) => {
let fo;
if (edge.startLabelLeft) {
// Create the actual text element
const startLabelElement = createLabel(edge.startLabelLeft, edge.labelStyle);
const startLabelElement = await createLabel(edge.startLabelLeft, edge.labelStyle);
const startEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
const inner = startEdgeLabelLeft.insert('g').attr('class', 'inner');
fo = inner.node().appendChild(startLabelElement);
Expand All @@ -77,7 +77,7 @@ export const insertEdgeLabel = (elem, edge) => {
}
if (edge.startLabelRight) {
// Create the actual text element
const startLabelElement = createLabel(edge.startLabelRight, edge.labelStyle);
const startLabelElement = await createLabel(edge.startLabelRight, edge.labelStyle);
const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
const inner = startEdgeLabelRight.insert('g').attr('class', 'inner');
fo = startEdgeLabelRight.node().appendChild(startLabelElement);
Expand All @@ -93,7 +93,7 @@ export const insertEdgeLabel = (elem, edge) => {
}
if (edge.endLabelLeft) {
// Create the actual text element
const endLabelElement = createLabel(edge.endLabelLeft, edge.labelStyle);
const endLabelElement = await createLabel(edge.endLabelLeft, edge.labelStyle);
const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner');
fo = inner.node().appendChild(endLabelElement);
Expand All @@ -110,7 +110,7 @@ export const insertEdgeLabel = (elem, edge) => {
}
if (edge.endLabelRight) {
// Create the actual text element
const endLabelElement = createLabel(edge.endLabelRight, edge.labelStyle);
const endLabelElement = await createLabel(edge.endLabelRight, edge.labelStyle);
const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
const inner = endEdgeLabelRight.insert('g').attr('class', 'inner');

Expand Down
6 changes: 3 additions & 3 deletions packages/mermaid/src/dagre-wrapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
// Move the nodes to the correct place
let diff = 0;
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
sortNodesByHierarchy(graph).forEach(function (v) {
for (const v of sortNodesByHierarchy(graph)) {
const node = graph.node(v);
log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v)));
log.info(
Expand All @@ -141,14 +141,14 @@ const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, sit
// A cluster in the non-recursive way
// positionCluster(node);
node.height += subGraphTitleTotalMargin;
insertCluster(clusters, node);
await insertCluster(clusters, node);
clusterDb[node.id].node = node;
} else {
node.y += subGraphTitleTotalMargin / 2;
positionNode(node);
}
}
});
}

// Move the edge labels to the correct place after layout
graph.edges().forEach(function (e) {
Expand Down
25 changes: 15 additions & 10 deletions packages/mermaid/src/dagre-wrapper/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ function applyNodePropertyBorders(rect, borders, totalWidth, totalHeight) {
rect.attr('stroke-dasharray', strokeDashArray.join(' '));
}

const rectWithTitle = (parent, node) => {
const rectWithTitle = async (parent, node) => {
// const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes);

let classes;
Expand Down Expand Up @@ -586,7 +586,7 @@ const rectWithTitle = (parent, node) => {
}
log.info('Label text abc79', title, text2, typeof text2 === 'object');

const text = label.node().appendChild(createLabel(title, node.labelStyle, true, true));
const text = label.node().appendChild(await createLabel(title, node.labelStyle, true, true));
let bbox = { width: 0, height: 0 };
if (evaluate(getConfig().flowchart.htmlLabels)) {
const div = text.children[0];
Expand All @@ -601,7 +601,12 @@ const rectWithTitle = (parent, node) => {
const descr = label
.node()
.appendChild(
createLabel(textRows.join ? textRows.join('<br/>') : textRows, node.labelStyle, true, true)
await createLabel(
textRows.join ? textRows.join('<br/>') : textRows,
node.labelStyle,
true,
true
)
);

if (evaluate(getConfig().flowchart.htmlLabels)) {
Expand Down Expand Up @@ -876,7 +881,7 @@ const end = (parent, node) => {
return shapeSvg;
};

const class_box = (parent, node) => {
const class_box = async (parent, node) => {
const halfPadding = node.padding / 2;
const rowPadding = 4;
const lineHeight = 8;
Expand Down Expand Up @@ -910,7 +915,7 @@ const class_box = (parent, node) => {
: '';
const interfaceLabel = labelContainer
.node()
.appendChild(createLabel(interfaceLabelText, node.labelStyle, true, true));
.appendChild(await createLabel(interfaceLabelText, node.labelStyle, true, true));
let interfaceBBox = interfaceLabel.getBBox();
if (evaluate(getConfig().flowchart.htmlLabels)) {
const div = interfaceLabel.children[0];
Expand All @@ -935,7 +940,7 @@ const class_box = (parent, node) => {
}
const classTitleLabel = labelContainer
.node()
.appendChild(createLabel(classTitleString, node.labelStyle, true, true));
.appendChild(await createLabel(classTitleString, node.labelStyle, true, true));
select(classTitleLabel).attr('class', 'classTitle');
let classTitleBBox = classTitleLabel.getBBox();
if (evaluate(getConfig().flowchart.htmlLabels)) {
Expand All @@ -950,7 +955,7 @@ const class_box = (parent, node) => {
maxWidth = classTitleBBox.width;
}
const classAttributes = [];
node.classData.members.forEach((member) => {
node.classData.members.forEach(async (member) => {
const parsedInfo = member.getDisplayDetails();
let parsedText = parsedInfo.displayText;
if (getConfig().flowchart.htmlLabels) {
Expand All @@ -959,7 +964,7 @@ const class_box = (parent, node) => {
const lbl = labelContainer
.node()
.appendChild(
createLabel(
await createLabel(
parsedText,
parsedInfo.cssStyle ? parsedInfo.cssStyle : node.labelStyle,
true,
Expand All @@ -984,7 +989,7 @@ const class_box = (parent, node) => {
maxHeight += lineHeight;

const classMethods = [];
node.classData.methods.forEach((member) => {
node.classData.methods.forEach(async (member) => {
const parsedInfo = member.getDisplayDetails();
let displayText = parsedInfo.displayText;
if (getConfig().flowchart.htmlLabels) {
Expand All @@ -993,7 +998,7 @@ const class_box = (parent, node) => {
const lbl = labelContainer
.node()
.appendChild(
createLabel(
await createLabel(
displayText,
parsedInfo.cssStyle ? parsedInfo.cssStyle : node.labelStyle,
true,
Expand Down
7 changes: 6 additions & 1 deletion packages/mermaid/src/dagre-wrapper/shapes/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
);
} else {
text = textNode.appendChild(
createLabel(sanitizeText(decodeEntities(labelText), config), node.labelStyle, false, isNode)
await createLabel(
sanitizeText(decodeEntities(labelText), config),
node.labelStyle,
false,
isNode
)
);
}
// Get the size of the label
Expand Down
14 changes: 14 additions & 0 deletions packages/mermaid/src/diagrams/block/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,20 @@ const getStyles = (options: BlockChartStyleOptions) =>
font-size: 18px;
fill: ${options.textColor};
}
.node label-icon path {
fill: currentColor;
stroke: revert;
stroke-width: revert;
}
/**
* These are copied from font-awesome.css
*/
.label-icon {
display: inline-block;
height: 1em;
overflow: visible;
vertical-align: -0.125em;
}
`;

export default getStyles;
Loading
Loading