diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 71abdfdb4..325985c7c 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -68,6 +68,10 @@ }, "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", diff --git a/packages/mermaid/src/rendering-util/createText.spec.ts b/packages/mermaid/src/rendering-util/createText.spec.ts index da0505ad8..7229072ce 100644 --- a/packages/mermaid/src/rendering-util/createText.spec.ts +++ b/packages/mermaid/src/rendering-util/createText.spec.ts @@ -1,12 +1,14 @@ import { describe, it, expect } 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'; describe('replaceIconSubstring', () => { it('converts FontAwesome icon notations to HTML tags', () => { const input = 'This is an icon: fa:fa-user and fab:fa-github'; const output = replaceIconSubstring(input); - const expected = - "This is an icon: and "; + const expected = `This is an icon: ${icon(faUser).html.join('')} and ${icon(faGithub).html.join('')}`; expect(output).toEqual(expected); }); @@ -19,8 +21,7 @@ describe('replaceIconSubstring', () => { it('correctly processes multiple FontAwesome icon notations in one string', () => { const input = 'Icons galore: fa:fa-arrow-right, fak:fa-truck, fas:fa-home'; const output = replaceIconSubstring(input); - const expected = - "Icons galore: , , "; + const expected = `Icons galore: ${icon(faArrowRight).html.join()}, , ${icon(faHome).html.join()}`; expect(output).toEqual(expected); }); diff --git a/packages/mermaid/src/rendering-util/createText.ts b/packages/mermaid/src/rendering-util/createText.ts index cc189e46e..3fa01a777 100644 --- a/packages/mermaid/src/rendering-util/createText.ts +++ b/packages/mermaid/src/rendering-util/createText.ts @@ -11,6 +11,22 @@ import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdo import { decodeEntities } from '../utils.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) { @@ -180,14 +196,36 @@ 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 i tags if they are from pro pack and as svg if they are from free pack */ -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) => `` - ); +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']; + + 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 ``; + } + const faIcon = icon({ prefix: prefix, iconName: iconName }); + if (!faIcon) { + log.warn(`Icon ${prefix}:fa-${iconName} not found.`); + return match; + } + + return faIcon.html.join(''); + }); } // 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' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df09304fa..178fb27ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,6 +217,18 @@ 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 @@ -2225,6 +2237,26 @@ 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==} @@ -12533,6 +12565,24 @@ 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':