mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 06:19:24 +02:00
Merge branch 'saurabh/refactor-fontawesome-icon-usage' of github.com-mermaid:mermaid-js/mermaid into saurabh/refactor-fontawesome-icon-usage
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
Free fontawesome icons are now embeded as svg inside diagram. Pro icons will still be using <i> tag.
|
||||
Registered icons are now embedded as SVGs inside diagram. If an icon is not available in the registered icons it will still use <i> tag
|
||||
|
32
cypress/integration/rendering/flowchart-icon.spec.js
Normal file
32
cypress/integration/rendering/flowchart-icon.spec.js
Normal file
@@ -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 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -6,6 +6,10 @@
|
||||
href="https://fonts.googleapis.com/css?family=Noto+Sans+SC&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
||||
/>
|
||||
<style>
|
||||
svg {
|
||||
border: 2px solid darkred;
|
||||
|
@@ -51,7 +51,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"/>',
|
||||
|
@@ -1916,9 +1916,13 @@ If a class is named default it will be assigned to all classes without specific
|
||||
|
||||
## Basic support for fontawesome
|
||||
|
||||
It is possible to add icons from fontawesome.
|
||||
It is possible to add icons from fontawesome and registered icon pack.
|
||||
|
||||
The icons are accessed via the syntax fa:#icon class name#.
|
||||
Mermaid supports icons from registered icon packs. Follow the instructions provided [here](../config/icons.md) to register your icon packs.
|
||||
|
||||
The registered icons can be accessed via the syntax #registered icon pack name#:#icon name#.
|
||||
|
||||
The fontawesome icons are accessed via the syntax fa:#icon class name#.
|
||||
|
||||
```mermaid-example
|
||||
flowchart TD
|
||||
|
@@ -68,10 +68,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@iconify/utils": "^2.1.32",
|
||||
"@mermaid-js/parser": "workspace:^",
|
||||
"@types/d3": "^7.4.3",
|
||||
|
@@ -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
|
||||
|
@@ -142,11 +142,20 @@ const getStyles = (options: BlockChartStyleOptions) =>
|
||||
font-size: 18px;
|
||||
fill: ${options.textColor};
|
||||
}
|
||||
.node .svg-inline--fa path {
|
||||
.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;
|
||||
|
@@ -157,10 +157,19 @@ g.classGroup line {
|
||||
font-size: 18px;
|
||||
fill: ${options.textColor};
|
||||
}
|
||||
.node .svg-inline--fa path {
|
||||
.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;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@@ -177,10 +177,19 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
.node .svg-inline--fa path {
|
||||
.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;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@@ -105,10 +105,19 @@ const getStyles: DiagramStylesProvider = (options) =>
|
||||
dominant-baseline: middle;
|
||||
text-align: center;
|
||||
}
|
||||
.node .svg-inline--fa path {
|
||||
.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;
|
||||
|
@@ -131,11 +131,20 @@ const getStyles = (options) =>
|
||||
.actor-5 {
|
||||
${options.actor5 ? `fill: ${options.actor5}` : ''};
|
||||
}
|
||||
.node .svg-inline--fa path {
|
||||
.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;
|
||||
|
@@ -1231,9 +1231,13 @@ If a class is named default it will be assigned to all classes without specific
|
||||
|
||||
## Basic support for fontawesome
|
||||
|
||||
It is possible to add icons from fontawesome.
|
||||
It is possible to add icons from fontawesome and registered icon pack.
|
||||
|
||||
The icons are accessed via the syntax fa:#icon class name#.
|
||||
Mermaid supports icons from registered icon packs. Follow the instructions provided [here](../config/icons.md) to register your icon packs.
|
||||
|
||||
The registered icons can be accessed via the syntax #registered icon pack name#:#icon name#.
|
||||
|
||||
The fontawesome icons are accessed via the syntax fa:#icon class name#.
|
||||
|
||||
```mermaid-example
|
||||
flowchart TD
|
||||
|
@@ -1,35 +1,57 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { replaceIconSubstring } from './createText.js';
|
||||
import { icon } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faUser, faArrowRight, faHome } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
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: ${icon(faUser).html.join('')} and ${icon(faGithub).html.join('')}`;
|
||||
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: ${icon(faArrowRight).html.join()}, <i class='fak fa-truck'></i>, ${icon(faHome).html.join()}`;
|
||||
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 () => {
|
||||
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,32 +1,17 @@
|
||||
/* 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 } 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';
|
||||
import { library, icon } from '@fortawesome/fontawesome-svg-core';
|
||||
import * as fab from '@fortawesome/free-brands-svg-icons';
|
||||
import * as fas from '@fortawesome/free-solid-svg-icons';
|
||||
import * as far from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
const iconListFab = Object.keys(fab)
|
||||
.filter((key) => key !== 'fab' && key !== 'prefix')
|
||||
.map((icon) => fab[icon]);
|
||||
const iconListFas = Object.keys(fas)
|
||||
.filter((key) => key !== 'fas' && key !== 'prefix')
|
||||
.map((icon) => fas[icon]);
|
||||
const iconListFar = Object.keys(far)
|
||||
.filter((key) => key !== 'far' && key !== 'prefix')
|
||||
.map((icon) => far[icon]);
|
||||
|
||||
library.add(...iconListFab, ...iconListFas, ...iconListFar);
|
||||
|
||||
function applyStyle(dom, styleFn) {
|
||||
if (styleFn) {
|
||||
@@ -196,36 +181,35 @@ 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 if they are from pro pack and as svg if they are from free pack
|
||||
* @returns string with fontawesome icons as svg if the icon is registered otherwise as i tags
|
||||
*/
|
||||
export function replaceIconSubstring(text) {
|
||||
const iconRegex = /(fas|fab|far|fa|fal|fak|fad):fa-([a-z-]+)/g;
|
||||
const classNameMap = {
|
||||
fas: 'fa-solid',
|
||||
fab: 'fa-brands',
|
||||
far: 'fa-regular',
|
||||
fa: 'fa',
|
||||
fal: 'fa-light',
|
||||
fad: 'fa-duotone',
|
||||
fak: 'fak',
|
||||
} as const;
|
||||
const freeIconPack = ['fas', 'fab', 'far', 'fa'];
|
||||
export async 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
|
||||
const iconRegex = /(fa[bklrs]?):fa-([\w-]+)/g; // cspell: disable-line
|
||||
|
||||
return text.replace(iconRegex, (match, prefix, iconName) => {
|
||||
const isFreeIcon = freeIconPack.includes(prefix);
|
||||
const className = classNameMap[prefix];
|
||||
if (!isFreeIcon) {
|
||||
log.warn(`Icon ${prefix}:fa-${iconName} is pro icon.`);
|
||||
return `<i class='${className} fa-${iconName}'></i>`;
|
||||
}
|
||||
const faIcon = icon({ prefix: prefix, iconName: iconName });
|
||||
if (!faIcon) {
|
||||
log.warn(`Icon ${prefix}:fa-${iconName} not found.`);
|
||||
return match;
|
||||
}
|
||||
const matches = [...text.matchAll(iconRegex)];
|
||||
if (matches.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return faIcon.html.join('');
|
||||
});
|
||||
let newText = text;
|
||||
|
||||
for (const match of matches) {
|
||||
const [fullMatch, prefix, iconName] = match;
|
||||
const registeredIconName = `${prefix}:${iconName}`;
|
||||
try {
|
||||
const isIconAvail = await isIconAvailable(registeredIconName);
|
||||
if (isIconAvail) {
|
||||
const faIcon = await getIconSVG(registeredIconName, undefined, { class: 'label-icon' });
|
||||
newText = newText.replace(fullMatch, faIcon);
|
||||
} else {
|
||||
newText = newText.replace(fullMatch, `<i class='${fullMatch.replace(':', ' ')}'></i>`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Error processing ${registeredIconName}:`, error);
|
||||
}
|
||||
}
|
||||
return newText;
|
||||
}
|
||||
|
||||
// Note when using from flowcharts converting the API isNode means classes should be set accordingly. When using htmlLabels => to sett classes to'nodeLabel' when isNode=true otherwise 'edgeLabel'
|
||||
@@ -259,7 +243,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;
|
||||
};
|
||||
|
72
pnpm-lock.yaml
generated
72
pnpm-lock.yaml
generated
@@ -220,18 +220,6 @@ importers:
|
||||
'@braintree/sanitize-url':
|
||||
specifier: ^7.0.1
|
||||
version: 7.1.0
|
||||
'@fortawesome/fontawesome-svg-core':
|
||||
specifier: ^6.7.2
|
||||
version: 6.7.2
|
||||
'@fortawesome/free-brands-svg-icons':
|
||||
specifier: ^6.7.2
|
||||
version: 6.7.2
|
||||
'@fortawesome/free-regular-svg-icons':
|
||||
specifier: ^6.7.2
|
||||
version: 6.7.2
|
||||
'@fortawesome/free-solid-svg-icons':
|
||||
specifier: ^6.7.2
|
||||
version: 6.7.2
|
||||
'@iconify/utils':
|
||||
specifier: ^2.1.32
|
||||
version: 2.1.33
|
||||
@@ -2589,26 +2577,6 @@ packages:
|
||||
'@floating-ui/vue@1.1.5':
|
||||
resolution: {integrity: sha512-ynL1p5Z+woPVSwgMGqeDrx6HrJfGIDzFyESFkyqJKilGW1+h/8yVY29Khn0LaU6wHBRwZ13ntG6reiHWK6jyzw==}
|
||||
|
||||
'@fortawesome/fontawesome-common-types@6.7.2':
|
||||
resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
'@fortawesome/fontawesome-svg-core@6.7.2':
|
||||
resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
'@fortawesome/free-brands-svg-icons@6.7.2':
|
||||
resolution: {integrity: sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
'@fortawesome/free-regular-svg-icons@6.7.2':
|
||||
resolution: {integrity: sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
'@fortawesome/free-solid-svg-icons@6.7.2':
|
||||
resolution: {integrity: sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
'@hapi/hoek@9.3.0':
|
||||
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
|
||||
|
||||
@@ -13246,24 +13214,6 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@fortawesome/fontawesome-common-types@6.7.2': {}
|
||||
|
||||
'@fortawesome/fontawesome-svg-core@6.7.2':
|
||||
dependencies:
|
||||
'@fortawesome/fontawesome-common-types': 6.7.2
|
||||
|
||||
'@fortawesome/free-brands-svg-icons@6.7.2':
|
||||
dependencies:
|
||||
'@fortawesome/fontawesome-common-types': 6.7.2
|
||||
|
||||
'@fortawesome/free-regular-svg-icons@6.7.2':
|
||||
dependencies:
|
||||
'@fortawesome/fontawesome-common-types': 6.7.2
|
||||
|
||||
'@fortawesome/free-solid-svg-icons@6.7.2':
|
||||
dependencies:
|
||||
'@fortawesome/fontawesome-common-types': 6.7.2
|
||||
|
||||
'@hapi/hoek@9.3.0': {}
|
||||
|
||||
'@hapi/topo@5.1.0':
|
||||
@@ -13313,7 +13263,7 @@ snapshots:
|
||||
'@antfu/install-pkg': 0.4.1
|
||||
'@antfu/utils': 0.7.10
|
||||
'@iconify/types': 2.0.0
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
kolorist: 1.8.0
|
||||
local-pkg: 0.5.0
|
||||
mlly: 1.7.2
|
||||
@@ -13594,7 +13544,7 @@ snapshots:
|
||||
|
||||
'@jridgewell/source-map@0.3.6':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/gen-mapping': 0.3.8
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||
@@ -14773,7 +14723,7 @@ snapshots:
|
||||
'@vueuse/shared': 10.11.1(vue@3.5.11(typescript@5.4.5))
|
||||
vue-demi: 0.14.10(vue@3.5.11(typescript@5.4.5))
|
||||
optionalDependencies:
|
||||
axios: 1.7.7
|
||||
axios: 1.7.7(debug@4.3.7)
|
||||
focus-trap: 7.6.0
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
@@ -15011,7 +14961,7 @@ snapshots:
|
||||
|
||||
agent-base@7.1.1:
|
||||
dependencies:
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -15222,7 +15172,7 @@ snapshots:
|
||||
|
||||
axios@1.7.7:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.9(debug@4.4.0)
|
||||
follow-redirects: 1.15.9(debug@4.3.7)
|
||||
form-data: 4.0.0
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -16436,6 +16386,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
debug@4.3.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.3.7(supports-color@8.1.1):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -17566,7 +17520,7 @@ snapshots:
|
||||
|
||||
follow-redirects@1.15.9(debug@4.3.7):
|
||||
optionalDependencies:
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
|
||||
follow-redirects@1.15.9(debug@4.4.0):
|
||||
optionalDependencies:
|
||||
@@ -18008,7 +17962,7 @@ snapshots:
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -18064,7 +18018,7 @@ snapshots:
|
||||
https-proxy-agent@7.0.5:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -21202,7 +21156,7 @@ snapshots:
|
||||
arg: 5.0.2
|
||||
bluebird: 3.7.2
|
||||
check-more-types: 2.24.0
|
||||
debug: 4.3.7(supports-color@8.1.1)
|
||||
debug: 4.3.7
|
||||
execa: 5.1.1
|
||||
lazy-ass: 1.6.0
|
||||
ps-tree: 1.2.0
|
||||
|
Reference in New Issue
Block a user