mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-01 11:24:16 +01:00
Merge pull request #6282 from mermaid-js/saurabh/refactor-fontawesome-icon-usage
Refactor fontawesome icon usage.
This commit is contained in:
5
.changeset/proud-seahorses-wash.md
Normal file
5
.changeset/proud-seahorses-wash.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
FontAwesome icons can now be embedded as SVGs in flowcharts if they are registered via `mermaid.registerIconPacks`.
|
||||
@@ -26,6 +26,7 @@ dompurify
|
||||
elkjs
|
||||
fcose
|
||||
fontawesome
|
||||
Fonticons
|
||||
Forgejo
|
||||
Foswiki
|
||||
Gitea
|
||||
|
||||
28
cypress/integration/rendering/flowchart-icon.spec.js
Normal file
28
cypress/integration/rendering/flowchart-icon.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { imgSnapshotTest } from '../../helpers/util.ts';
|
||||
|
||||
const themes = ['default', 'forest', 'dark', 'base', 'neutral'];
|
||||
|
||||
describe('when rendering flowchart with icons', () => {
|
||||
for (const theme of themes) {
|
||||
it(`should render icons from fontawesome library on 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 }
|
||||
);
|
||||
});
|
||||
|
||||
it(`should render registered icons on theme ${theme}`, () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart TD
|
||||
A("fa:fa-bell Bell")
|
||||
`,
|
||||
{ theme }
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -99,7 +99,7 @@ describe('Flowchart v2', () => {
|
||||
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);
|
||||
expect(maxWidthValue).to.be.within(440 * 0.95, 440 * 1.05);
|
||||
});
|
||||
});
|
||||
it('8: should render a flowchart when useMaxWidth is false', () => {
|
||||
@@ -118,7 +118,7 @@ describe('Flowchart v2', () => {
|
||||
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(width).to.be.within(440 * 0.95, 440 * 1.05);
|
||||
expect(svg).to.not.have.attr('style');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
svg {
|
||||
svg:not(svg svg) {
|
||||
border: 2px solid darkred;
|
||||
}
|
||||
.exClass2 > rect,
|
||||
|
||||
@@ -14,12 +14,28 @@ 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 () {
|
||||
await loadFontAwesomeCSS();
|
||||
await Promise.all(Array.from(document.fonts, (font) => font.load()));
|
||||
|
||||
let pos = document.location.href.indexOf('?graph=');
|
||||
if (pos > 0) {
|
||||
pos = pos + 7;
|
||||
@@ -50,8 +66,13 @@ const contentLoaded = async function () {
|
||||
|
||||
mermaid.registerLayoutLoaders(layouts);
|
||||
mermaid.initialize(graphObj.mermaid);
|
||||
/**
|
||||
* CC-BY-4.0
|
||||
* Copyright (c) Fonticons, Inc. - https://fontawesome.com/license/free
|
||||
* https://fontawesome.com/icons/bell?f=classic&s=regular
|
||||
*/
|
||||
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"/>',
|
||||
|
||||
@@ -1950,6 +1950,19 @@ flowchart TD
|
||||
B-->E(A fa:fa-camera-retro perhaps?)
|
||||
```
|
||||
|
||||
There are two ways to display these FontAwesome icons:
|
||||
|
||||
### Register FontAwesome icon packs (v\<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
You can register your own FontAwesome icon pack following the ["Registering icon packs" instructions](../config/icons.md).
|
||||
|
||||
Supported prefixes: `fa`, `fab`, `fas`, `far`, `fal`, `fad`.
|
||||
|
||||
> **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.
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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');
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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];
|
||||
@@ -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)) {
|
||||
@@ -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;
|
||||
@@ -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];
|
||||
@@ -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)) {
|
||||
@@ -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) {
|
||||
@@ -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,
|
||||
@@ -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) {
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as khroma from 'khroma';
|
||||
import { getIconStyles } from '../globalStyles.js';
|
||||
|
||||
/** Returns the styles given options */
|
||||
export interface BlockChartStyleOptions {
|
||||
@@ -142,6 +143,7 @@ const getStyles = (options: BlockChartStyleOptions) =>
|
||||
font-size: 18px;
|
||||
fill: ${options.textColor};
|
||||
}
|
||||
${getIconStyles()}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getIconStyles } from '../globalStyles.js';
|
||||
|
||||
const getStyles = (options) =>
|
||||
`g.classGroup text {
|
||||
fill: ${options.nodeBorder || options.classText};
|
||||
@@ -157,6 +159,7 @@ g.classGroup line {
|
||||
font-size: 18px;
|
||||
fill: ${options.textColor};
|
||||
}
|
||||
${getIconStyles()}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// import khroma from 'khroma';
|
||||
import * as khroma from 'khroma';
|
||||
import { getIconStyles } from '../globalStyles.js';
|
||||
|
||||
/** Returns the styles given options */
|
||||
export interface FlowChartStyleOptions {
|
||||
@@ -177,6 +178,7 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
${getIconStyles()}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
||||
15
packages/mermaid/src/diagrams/globalStyles.ts
Normal file
15
packages/mermaid/src/diagrams/globalStyles.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const getIconStyles = () => `
|
||||
/* Font Awesome icon styling - consolidated */
|
||||
.label-icon {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
overflow: visible;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
|
||||
.node .label-icon path {
|
||||
fill: currentColor;
|
||||
stroke: revert;
|
||||
stroke-width: revert;
|
||||
}
|
||||
`;
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-expect-error Incorrect khroma types
|
||||
import { darken, lighten, isDark } from 'khroma';
|
||||
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
|
||||
import { getIconStyles } from '../globalStyles.js';
|
||||
|
||||
const genSections: DiagramStylesProvider = (options) => {
|
||||
let sections = '';
|
||||
@@ -105,5 +106,6 @@ const getStyles: DiagramStylesProvider = (options) =>
|
||||
dominant-baseline: middle;
|
||||
text-align: center;
|
||||
}
|
||||
${getIconStyles()}
|
||||
`;
|
||||
export default getStyles;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getIconStyles } from '../globalStyles.js';
|
||||
|
||||
const getStyles = (options) =>
|
||||
`.label {
|
||||
font-family: ${options.fontFamily};
|
||||
@@ -131,6 +133,7 @@ const getStyles = (options) =>
|
||||
.actor-5 {
|
||||
${options.actor5 ? `fill: ${options.actor5}` : ''};
|
||||
}
|
||||
${getIconStyles()}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
|
||||
@@ -1250,6 +1250,20 @@ flowchart TD
|
||||
B-->E(A fa:fa-camera-retro perhaps?)
|
||||
```
|
||||
|
||||
There are two ways to display these FontAwesome icons:
|
||||
|
||||
### Register FontAwesome icon packs (v<MERMAID_RELEASE_VERSION>+)
|
||||
|
||||
You can register your own FontAwesome icon pack following the ["Registering icon packs" instructions](../config/icons.md).
|
||||
|
||||
Supported prefixes: `fa`, `fab`, `fas`, `far`, `fal`, `fad`.
|
||||
|
||||
```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.
|
||||
|
||||
|
||||
@@ -1,34 +1,62 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { replaceIconSubstring } from './createText.js';
|
||||
import mermaid from '../mermaid.js';
|
||||
|
||||
describe('replaceIconSubstring', () => {
|
||||
it('converts FontAwesome icon notations to HTML tags', () => {
|
||||
it('converts FontAwesome icon notations to HTML tags', async () => {
|
||||
const input = 'This is an icon: fa:fa-user and fab:fa-github';
|
||||
const output = replaceIconSubstring(input);
|
||||
const expected =
|
||||
"This is an icon: <i class='fa fa-user'></i> and <i class='fab fa-github'></i>";
|
||||
const output = await replaceIconSubstring(input);
|
||||
const expected = `This is an icon: <i class='fa fa-user'></i> and <i class='fab fa-github'></i>`;
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles strings without FontAwesome icon notations', () => {
|
||||
it('handles strings without FontAwesome icon notations', async () => {
|
||||
const input = 'This string has no icons';
|
||||
const output = replaceIconSubstring(input);
|
||||
const output = await replaceIconSubstring(input);
|
||||
expect(output).toEqual(input); // No change expected
|
||||
});
|
||||
|
||||
it('correctly processes multiple FontAwesome icon notations in one string', () => {
|
||||
it('correctly processes multiple FontAwesome icon notations in one string', async () => {
|
||||
const input = 'Icons galore: fa:fa-arrow-right, fak:fa-truck, fas:fa-home';
|
||||
const output = replaceIconSubstring(input);
|
||||
const expected =
|
||||
"Icons galore: <i class='fa fa-arrow-right'></i>, <i class='fak fa-truck'></i>, <i class='fas fa-home'></i>";
|
||||
const output = await replaceIconSubstring(input);
|
||||
const expected = `Icons galore: <i class='fa fa-arrow-right'></i>, <i class='fak fa-truck'></i>, <i class='fas fa-home'></i>`;
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('correctly replaces a very long icon name with the fak prefix', () => {
|
||||
it('correctly replaces a very long icon name with the fak prefix', async () => {
|
||||
const input = 'Here is a long icon: fak:fa-truck-driving-long-winding-road in use';
|
||||
const output = replaceIconSubstring(input);
|
||||
const output = await replaceIconSubstring(input);
|
||||
const expected =
|
||||
"Here is a long icon: <i class='fak fa-truck-driving-long-winding-road'></i> in use";
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('correctly process the registered icons', async () => {
|
||||
/**
|
||||
* CC-BY-4.0
|
||||
* Copyright (c) Fonticons, Inc. - https://fontawesome.com/license/free
|
||||
* https://fontawesome.com/icons/bell?f=classic&s=regular
|
||||
*/
|
||||
const staticBellIconPack = {
|
||||
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"/>',
|
||||
width: 448,
|
||||
},
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
};
|
||||
mermaid.registerIconPacks([
|
||||
{
|
||||
name: 'fa',
|
||||
loader: () => Promise.resolve(staticBellIconPack),
|
||||
},
|
||||
]);
|
||||
const input = 'Icons galore: fa:fa-bell';
|
||||
const output = await replaceIconSubstring(input);
|
||||
const expected = staticBellIconPack.icons.bell.body;
|
||||
expect(output).toContain(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// @ts-nocheck TODO: Fix types
|
||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||
import common, { hasKatex, renderKatex } from '../diagrams/common/common.js';
|
||||
import { select } from 'd3';
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
import { getConfig, sanitizeText } from '../diagram-api/diagramAPI.js';
|
||||
import type { SVGGroup } from '../diagram-api/types.js';
|
||||
import common, { hasKatex, renderKatex } from '../diagrams/common/common.js';
|
||||
import type { D3TSpanElement, D3TextElement } from '../diagrams/common/commonTypes.js';
|
||||
import { log } from '../logger.js';
|
||||
import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js';
|
||||
import { decodeEntities } from '../utils.js';
|
||||
import { getIconSVG, isIconAvailable } from './icons.js';
|
||||
import { splitLineToFitWidth } from './splitText.js';
|
||||
import type { MarkdownLine, MarkdownWord } from './types.js';
|
||||
|
||||
@@ -180,14 +181,28 @@ function updateTextContentAndStyles(tspan: any, wrappedLine: MarkdownWord[]) {
|
||||
/**
|
||||
* Convert fontawesome labels into fontawesome icons by using a regex pattern
|
||||
* @param text - The raw string to convert
|
||||
* @returns string with fontawesome icons as i tags
|
||||
* @returns string with fontawesome icons as svg if the icon is registered otherwise as i tags
|
||||
*/
|
||||
export function replaceIconSubstring(text: string) {
|
||||
// The letters 'bklrs' stand for possible endings of the fontawesome prefix (e.g. 'fab' for brands, 'fak' for fa-kit) // cspell: disable-line
|
||||
return text.replace(
|
||||
/fa[bklrs]?:fa-[\w-]+/g, // cspell: disable-line
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
);
|
||||
export async function replaceIconSubstring(text: string) {
|
||||
const pendingReplacements: Promise<string>[] = [];
|
||||
// cspell: disable-next-line
|
||||
text.replace(/(fa[bklrs]?):fa-([\w-]+)/g, (fullMatch, prefix, iconName) => {
|
||||
pendingReplacements.push(
|
||||
(async () => {
|
||||
const registeredIconName = `${prefix}:${iconName}`;
|
||||
if (await isIconAvailable(registeredIconName)) {
|
||||
return await getIconSVG(registeredIconName, undefined, { class: 'label-icon' });
|
||||
} else {
|
||||
return `<i class='${sanitizeText(fullMatch).replace(':', ' ')}'></i>`;
|
||||
}
|
||||
})()
|
||||
);
|
||||
return fullMatch;
|
||||
});
|
||||
|
||||
const replacements = await Promise.all(pendingReplacements);
|
||||
// cspell: disable-next-line
|
||||
return text.replace(/(fa[bklrs]?):fa-([\w-]+)/g, () => replacements.shift() ?? '');
|
||||
}
|
||||
|
||||
// Note when using from flowcharts converting the API isNode means classes should be set accordingly. When using htmlLabels => to set classes to 'nodeLabel' when isNode=true otherwise 'edgeLabel'
|
||||
@@ -221,7 +236,7 @@ export const createText = async (
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
|
||||
const htmlText = markdownToHTML(text, config);
|
||||
const decodedReplacedText = replaceIconSubstring(decodeEntities(htmlText));
|
||||
const decodedReplacedText = await replaceIconSubstring(decodeEntities(htmlText));
|
||||
|
||||
//for Katex the text could contain escaped characters, \\relax that should be transformed to \relax
|
||||
const inputForKatex = text.replace(/\\\\/g, '\\');
|
||||
|
||||
@@ -85,7 +85,8 @@ export const isIconAvailable = async (iconName: string) => {
|
||||
|
||||
export const getIconSVG = async (
|
||||
iconName: string,
|
||||
customisations?: IconifyIconCustomisations & { fallbackPrefix?: string }
|
||||
customisations?: IconifyIconCustomisations & { fallbackPrefix?: string },
|
||||
extraAttributes?: Record<string, string>
|
||||
) => {
|
||||
let iconData: ExtendedIconifyIcon;
|
||||
try {
|
||||
@@ -95,6 +96,9 @@ export const getIconSVG = async (
|
||||
iconData = unknownIcon;
|
||||
}
|
||||
const renderData = iconToSVG(iconData, customisations);
|
||||
const svg = iconToHTML(replaceIDs(renderData.body), renderData.attributes);
|
||||
const svg = iconToHTML(replaceIDs(renderData.body), {
|
||||
...renderData.attributes,
|
||||
...extraAttributes,
|
||||
});
|
||||
return svg;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user