diff --git a/cSpell.json b/cSpell.json index fc5eb6f9dc..25c1e854f6 100644 --- a/cSpell.json +++ b/cSpell.json @@ -68,6 +68,7 @@ "jgreywolf", "jison", "jiti", + "katex", "kaufmann", "khroma", "klemm", @@ -81,6 +82,7 @@ "logmsg", "lucida", "markdownish", + "mathml", "matthieu", "matthieumorel", "mdast", diff --git a/cypress/integration/rendering/katex.spec.js b/cypress/integration/rendering/katex.spec.js new file mode 100644 index 0000000000..fb1d13392a --- /dev/null +++ b/cypress/integration/rendering/katex.spec.js @@ -0,0 +1,36 @@ +import { imgSnapshotTest } from '../../helpers/util'; + +describe('Katex', () => { + it('1: should render a complex Katex flowchart no htmlLabels', () => { + imgSnapshotTest( + `graph LR + A["$$f(\\relax{x}) = \\int_{-\\infty}^\\infty \\hat{f}(\\xi)\\,e^{2 \\pi i \\xi x}\\,d\\xi$$"] -->|"$$\\Bigg(\\bigg(\\Big(\\big((\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a})\\big)\\Big)\\bigg)\\Bigg)$$"| B("$$1+\\frac{e^{-2\\pi}} {1+\\frac{e^{-4\\pi}} {1+\\frac{e^{-6\\pi}} {1+\\frac{e^{-8\\pi}} {1+\\cdots}}}}$$") + A -->|"$$\\overbrace{a+b+c}^{\\text{note}}$$"| C("$$\\phase{-78^\\circ}$$") + B --> D("$$x = \\begin{cases} a &\\text{if } b \\\\ c &\\text{if } d \\end{cases}$$") + C --> E("$$x(t)=c_1\\begin{bmatrix}-\\cos{t}+\\sin{t}\\\\ 2\\cos{t} \\end{bmatrix}e^{2t}$$")`, + { fontFamily: 'courier' } + ); + }); + it('2: should render a Katex flowchart containing the Greek alphabet', () => { + imgSnapshotTest( + `graph LR + A["$$\\alpha\\beta\\gamma\\delta\\epsilon\\zeta\\eta\\theta\\iota\\kappa\\lambda\\mu\\nu\\xi\\omicron\\pi\\rho\\sigma\\tau\\upsilon\\phi\\chi\\psi\\omega$$"] --> B["$$\\Alpha\\Beta\\Gamma\\Delta\\Epsilon\\Zeta\\Eta\\Theta\\Iota\\Kappa\\Lambda\\Mu\\Nu\\Xi\\Omicron\\Pi\\Rho\\Sigma\\Tau\\Upsilon\\Phi\\Chi\\Psi\\Omega$$"]`, + { fontFamily: 'courier' } + ); + }); + it('3: should render a Katex flowchart containing set theory symbols', () => { + imgSnapshotTest( + `graph LR + A["$$\\forall\\complement\\therefore\\emptyset\\exists\\subset\\because\\empty\\exist\\supset\\mapsto\\varnothing\\nexists\\mid\\to\\implies\\in\\land\\gets\\impliedby\\isin\\lor\\leftrightarrow\\iff\\notin\\ni\\notni\\lnot$$"] --> B["$$\\nabla\\Im\\Reals\\jmath\\partial\\image\\wp\\aleph\\Game\\weierp\\alef\\Finv\\N\\Z\\alefsym\\cnums\\natnums\\beth\\Complex\\R\\gimel\\ell\\Re\\daleth\\hbar\\real\\eth\\hslash\\reals$$"]`, + { fontFamily: 'courier' } + ); + }); + // TODO: changes made to develop between Feb 13 - Feb 23 cause this test to no longer function + // it.skip('4: should render an error box originating from Katex', () => { + // imgSnapshotTest( + // `graph LR + // A["$$\\shouldBeError$$"]`, + // { fontFamily: 'courier' } + // ); + // }); +}); diff --git a/demos/flowchart.html b/demos/flowchart.html index d7032a663c..faf5337955 100644 --- a/demos/flowchart.html +++ b/demos/flowchart.html @@ -1102,6 +1102,57 @@

flowchart


+

Sample 20

+

graph

+
+      graph LR
+      A["$$f(\relax{x}) = \int_{-\infty}^\infty \hat{f}(\xi)\,e^{2 \pi i \xi x}\,d\xi$$"] -->|"$$\Bigg(\bigg(\Big(\big((\frac{-b\pm\sqrt{b^2-4ac}}{2a})\big)\Big)\bigg)\Bigg)$$"| B("$$1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots}}}}$$")
+      A -->|"$$\overbrace{a+b+c}^{\text{note}}$$"| C("$$\phase{-78^\circ}$$")
+      B --> D("$$x = \begin{cases} a &\text{if } b \\ c &\text{if } d \end{cases}$$")
+      C --> E("$$x(t)=c_1\begin{bmatrix}-\cos{t}+\sin{t}\\ 2\cos{t} \end{bmatrix}e^{2t}$$")
+    
+
+ +

flowchart

+
+      flowchart LR
+      A["$$f(\relax{x}) = \int_{-\infty}^\infty \hat{f}(\xi)\,e^{2 \pi i \xi x}\,d\xi$$"] -->|"$$\Bigg(\bigg(\Big(\big((\frac{-b\pm\sqrt{b^2-4ac}}{2a})\big)\Big)\bigg)\Bigg)$$"| B("$$1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots}}}}$$")
+      A -->|"$$\overbrace{a+b+c}^{\text{note}}$$"| C("$$\phase{-78^\circ}$$")
+      B --> D("$$x = \begin{cases} a &\text{if } b \\ c &\text{if } d \end{cases}$$")
+      C --> E("$$x(t)=c_1\begin{bmatrix}-\cos{t}+\sin{t}\\ 2\cos{t} \end{bmatrix}e^{2t}$$")
+    
+
+ +

Sample 21

+

graph

+
+      graph LR
+      A["$$\alpha\beta\gamma\delta\epsilon\zeta\eta\theta\iota\kappa\lambda\mu\nu\xi\omicron\pi\rho\sigma\tau\upsilon\phi\chi\psi\omega$$"] --> B["$$\Alpha\Beta\Gamma\Delta\Epsilon\Zeta\Eta\Theta\Iota\Kappa\Lambda\Mu\Nu\Xi\Omicron\Pi\Rho\Sigma\Tau\Upsilon\Phi\Chi\Psi\Omega$$"]
+    
+
+ +

flowchart

+
+      graph LR
+      A["$$\alpha\beta\gamma\delta\epsilon\zeta\eta\theta\iota\kappa\lambda\mu\nu\xi\omicron\pi\rho\sigma\tau\upsilon\phi\chi\psi\omega$$"] --> B["$$\Alpha\Beta\Gamma\Delta\Epsilon\Zeta\Eta\Theta\Iota\Kappa\Lambda\Mu\Nu\Xi\Omicron\Pi\Rho\Sigma\Tau\Upsilon\Phi\Chi\Psi\Omega$$"]
+    
+
+ +

Sample 22

+

graph

+
+      graph LR
+      A["$$\forall\complement\therefore\emptyset\exists\subset\because\empty\exist\supset\mapsto\varnothing\nexists\mid\to\implies\in\land\gets\impliedby\isin\lor\leftrightarrow\iff\notin\ni\notni\lnot$$"] --> B["$$\nabla\Im\Reals\jmath\partial\image\wp\aleph\Game\weierp\alef\Finv\N\Z\alefsym\cnums\natnums\beth\Complex\R\gimel\ell\Re\daleth\hbar\real\eth\hslash\reals$$"]
+    
+
+ +

flowchart

+
+      graph LR
+      A["$$\forall\complement\therefore\emptyset\exists\subset\because\empty\exist\supset\mapsto\varnothing\nexists\mid\to\implies\in\land\gets\impliedby\isin\lor\leftrightarrow\iff\notin\ni\notni\lnot$$"] --> B["$$\nabla\Im\Reals\jmath\partial\image\wp\aleph\Game\weierp\alef\Finv\N\Z\alefsym\cnums\natnums\beth\Complex\R\gimel\ell\Re\daleth\hbar\real\eth\hslash\reals$$"]
+    
+
+
@@ -1524,11 +1575,11 @@ 

flowchart

F{Flow 2} == Choice 2.1 ==> H[Feedback node] H[Feedback node] ==> B[Step 1] F{Flow 2} == Choice 2.2 ==> G((Finish)) - + linkStyle 0,1,4,6,7,8,9 stroke:gold, stroke-width:4px classDef active_node fill:#0CF,stroke:#09F,stroke-width:6px - classDef unactive_node fill:#e0e0e0,stroke:#bdbdbd,stroke-width:3px + classDef unactive_node fill:#e0e0e0,stroke:#bdbdbd,stroke-width:3px classDef bugged_node fill:#F88,stroke:#F22,stroke-width:3px classDef start_node,finish_node fill:#3B1,stroke:#391,stroke-width:8px diff --git a/demos/sequence.html b/demos/sequence.html index 8eecae6105..d6b4e3769d 100644 --- a/demos/sequence.html +++ b/demos/sequence.html @@ -16,9 +16,9 @@

Sequence diagram demos

-		sequenceDiagram
-			accTitle: test the accTitle
-			accDescr: Test a description
+    sequenceDiagram
+      accTitle: test the accTitle
+      accDescr: Test a description
 
 			participant Alice
 			participant Bob
@@ -31,39 +31,39 @@ 

Sequence diagram demos

rect rgb(200, 220, 100) rect rgb(200, 255, 200) - Alice ->> Bob: Hello Bob, how are you? - Bob-->>John: How about you John? - end + Alice ->> Bob: Hello Bob, how are you? + Bob-->>John: How about you John? + end - Bob--x Alice: I am good thanks! - Bob-x John: I am good thanks! - Note right of John: John thinks a long
long time, so long
that the text does
not fit on a row. + Bob--x Alice: I am good thanks! + Bob-x John: I am good thanks! + Note right of John: John thinks a long
long time, so long
that the text does
not fit on a row. - Bob-->Alice: Checking with John... - Note over John:wrap: John looks like he's still thinking, so Bob prods him a bit. - Bob-x John: Hey John - we're still waiting to know
how you're doing - Note over John:nowrap: John's trying hard not to break his train of thought. - Bob-x John:wrap: John! Are you still debating about how you're doing? How long does it take?? - Note over John: After a few more moments, John
finally snaps out of it. - end + Bob-->Alice: Checking with John... + Note over John:wrap: John looks like he's still thinking, so Bob prods him a bit. + Bob-x John: Hey John - we're still waiting to know
how you're doing + Note over John:nowrap: John's trying hard not to break his train of thought. + Bob-x John:wrap: John! Are you still debating about how you're doing? How long does it take?? + Note over John: After a few more moments, John
finally snaps out of it. + end - autonumber off - alt either this - Alice->>+John: Yes - John-->>-Alice: OK - else or this - autonumber - Alice->>John: No - else or this will happen - Alice->John: Maybe - end - autonumber 200 - par this happens in parallel - Alice -->> Bob: Parallel message 1 - and - Alice -->> John: Parallel message 2 - end -
+ autonumber off + alt either this + Alice->>+John: Yes + John-->>-Alice: OK + else or this + autonumber + Alice->>John: No + else or this will happen + Alice->John: Maybe + end + autonumber 200 + par this happens in parallel + Alice -->> Bob: Parallel message 1 + and + Alice -->> John: Parallel message 2 + end +

     ---
@@ -153,18 +153,18 @@ 

Sequence diagram demos


-      sequenceDiagram
-      box lightgreen Alice & John
-      participant A
-      participant J
-      end
-      box Another Group very very long description not wrapped
-      participant B
-      end
-      A->>J: Hello John, how are you?
-      J->>A: Great!
-      A->>B: Hello Bob, how are you ?
-      
>J: Hello John, how are you? + J->>A: Great! + A->>B: Hello Bob, how are you ? +

@@ -188,6 +188,49 @@

Sequence diagram demos

end +
+ +
+    sequenceDiagram
+    participant 1 as $$\frac{\lim_{x\rightarrow0}{\frac{1}{x}}}{\frac{-b\pm\sqrt{b^2-4ac}}{2a}}$$
+    participant 2 as $$\beta$$
+    participant 3 as $$\delta$$
+    participant 4 as $$\frac{\frac{\lim_{x\rightarrow0}{\frac{1}{x}}}{\frac{-b\pm\sqrt{b^2-4ac}}{2a}}}{\frac{\text{d}}{\text{d}x}{x^2}}$$
+    1->>2: $$\sqrt{2}$$
+    note right of 2: $$\frac{1+\frac{1+\frac{1+\frac{1}{2}}{2}}{2}}{2}+\frac{-b\pm\sqrt{b^2-4ac}}{2a}$$
+    2->>3: $$\frac{\lim_{x\rightarrow0}{\frac{1}{x}}}{\frac{-b\pm\sqrt{b^2-4ac}}{2a}}$$
+    note right of 3: $$\frac{-b\pm\sqrt{b^2-4ac}}{2a}$$
+    3->>4: $$\lim_{x\rightarrow0}{\frac{1}{x}}$$;
+    note right of 4: multiline
+    4->>1: multiline
using #lt;br /#gt; + note right of 1: multiline
$$\frac{1}{2}$$
3rd line +
+
+
+      sequenceDiagram
+      autonumber
+      participant 1 as $$\alpha$$lex
+      participant 2 as $$\beta$$ob
+      participant 3 as $$\theta$$iffany
+      1->>2: Hello John, does  $$\frac{1}{2}+1=2$$?
+      loop $$\frac{1}{2}+1=2$$
+          2->>2: $$\frac{1}{2}+1=\frac{3}{2}$$
+      end
+      Note right of 2: $$x = \begin{cases} 1 &\text{if } \frac{1}{2}+1=2 \\ 0 &\text{if } \frac{1}{2}+1\ne2 \end{cases}$$
+      2-->>1: $$\frac{1}{2}+1\ne2\implies 1$$
+      2->>3: $$\frac{\text{d}}{\text{d}x}{3x^2+2x+1}$$
+      3-->>2: $$6x+2$$
+    
+ +
+ +
+    sequenceDiagram
+      actor Alice
+      actor John
+      Alice-xJohn: Hello John, how are you?
+      John--xAlice: Great!
+    
+ + +``` diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 551e44ed93..6ba10ccffa 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -70,6 +70,7 @@ "dayjs": "^1.11.7", "dompurify": "^3.0.5", "elkjs": "^0.9.0", + "katex": "^0.16.9", "khroma": "^2.0.0", "lodash-es": "^4.17.21", "mdast-util-from-markdown": "^1.3.0", @@ -89,6 +90,7 @@ "@types/d3-shape": "^3.1.1", "@types/dompurify": "^3.0.2", "@types/jsdom": "^21.1.1", + "@types/katex": "^0.16.7", "@types/lodash-es": "^4.17.7", "@types/micromatch": "^4.0.2", "@types/prettier": "^2.7.2", diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 889e5ff254..0ba3178680 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -127,6 +127,14 @@ export interface MermaidConfig { * */ secure?: string[]; + /** + * This option specifies if Mermaid can expect the dependent to include KaTeX stylesheets for browsers + * without their own MathML implementation. If this option is disabled and MathML is not supported, the math + * equations are replaced with a warning. If this option is enabled and MathML is not supported, Mermaid will + * fall back to legacy rendering for KaTeX. + * + */ + legacyMathML?: boolean; /** * This option controls if the generated ids of nodes in the SVG are * generated randomly or based on a seed. diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts index 8609a175ae..04be2a5f46 100644 --- a/packages/mermaid/src/diagrams/common/common.ts +++ b/packages/mermaid/src/diagrams/common/common.ts @@ -289,6 +289,83 @@ const processSet = (input: string): string => { return chars.join(''); }; +// TODO: find a better method for detecting support. This interface was added in the MathML 4 spec. +// Firefox versions between [4,71] (0.47%) and Safari versions between [5,13.4] (0.17%) don't have this interface implemented but MathML is supported +export const isMathMLSupported = () => window.MathMLElement !== undefined; + +export const katexRegex = /\$\$(.*)\$\$/g; + +/** + * Whether or not a text has KaTeX delimiters + * + * @param text - The text to test + * @returns Whether or not the text has KaTeX delimiters + */ +export const hasKatex = (text: string): boolean => (text.match(katexRegex)?.length ?? 0) > 0; + +/** + * Computes the minimum dimensions needed to display a div containing MathML + * + * @param text - The text to test + * @param config - Configuration for Mermaid + * @returns Object containing \{width, height\} + */ +export const calculateMathMLDimensions = async (text: string, config: MermaidConfig) => { + text = await renderKatex(text, config); + const divElem = document.createElement('div'); + divElem.innerHTML = text; + divElem.id = 'katex-temp'; + divElem.style.visibility = 'hidden'; + divElem.style.position = 'absolute'; + divElem.style.top = '0'; + const body = document.querySelector('body'); + body?.insertAdjacentElement('beforeend', divElem); + const dim = { width: divElem.clientWidth, height: divElem.clientHeight }; + divElem.remove(); + return dim; +}; + +/** + * Attempts to render and return the KaTeX portion of a string with MathML + * + * @param text - The text to test + * @param config - Configuration for Mermaid + * @returns String containing MathML if KaTeX is supported, or an error message if it is not and stylesheets aren't present + */ +export const renderKatex = async (text: string, config: MermaidConfig): Promise => { + if (!hasKatex(text)) { + return text; + } + + if (!isMathMLSupported() && !config.legacyMathML) { + return text.replace(katexRegex, 'MathML is unsupported in this environment.'); + } + + const { default: katex } = await import('katex'); + return text + .split(lineBreakRegex) + .map((line) => + hasKatex(line) + ? ` +
+ ${line} +
+ ` + : `
${line}
` + ) + .join('') + .replace(katexRegex, (_, c) => + katex + .renderToString(c, { + throwOnError: true, + displayMode: true, + output: isMathMLSupported() ? 'mathml' : 'htmlAndMathml', + }) + .replace(/\n/g, ' ') + .replace(//g, '') + ); +}; + export default { getRows, sanitizeText, diff --git a/packages/mermaid/src/diagrams/error/errorRenderer.ts b/packages/mermaid/src/diagrams/error/errorRenderer.ts index 6ef00e300f..1b3622c6dc 100644 --- a/packages/mermaid/src/diagrams/error/errorRenderer.ts +++ b/packages/mermaid/src/diagrams/error/errorRenderer.ts @@ -4,7 +4,7 @@ import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { configureSvgSize } from '../../setupGraphViewbox.js'; /** - * Draws a an info picture in the tag with id: id based on the graph definition in text. + * Draws an info picture in the tag with id: id based on the graph definition in text. * * @param _text - Mermaid graph definition. * @param id - The text for the error @@ -12,12 +12,12 @@ import { configureSvgSize } from '../../setupGraphViewbox.js'; */ export const draw = (_text: string, id: string, version: string) => { log.debug('rendering svg for syntax error\n'); - const svg: SVG = selectSvgElement(id); + const g: Group = svg.append('g'); + svg.attr('viewBox', '0 0 2412 512'); configureSvgSize(svg, 100, 512, true); - const g: Group = svg.append('g'); g.append('path') .attr('class', 'error-icon') .attr( diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js index 2c505114c3..a82894f945 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js @@ -5,7 +5,7 @@ import utils from '../../utils.js'; import { render } from '../../dagre-wrapper/index.js'; import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js'; import { log } from '../../logger.js'; -import common, { evaluate } from '../common/common.js'; +import common, { evaluate, renderKatex } from '../common/common.js'; import { interpolateToCurve, getStylesFromArray } from '../../utils.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; @@ -27,12 +27,12 @@ export const setConf = function (cnf) { * @param doc * @param diagObj */ -export const addVertices = function (vert, g, svgId, root, doc, diagObj) { +export const addVertices = async function (vert, g, svgId, root, doc, diagObj) { const svg = root.select(`[id="${svgId}"]`); const keys = Object.keys(vert); // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - keys.forEach(function (id) { + for (const id of keys) { const vertex = vert[id]; /** @@ -59,10 +59,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) { if (evaluate(getConfig().flowchart.htmlLabels)) { // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? const node = { - label: vertexText.replace( - /fa[blrs]?:fa-[\w-]+/g, - (s) => `` - ), + label: vertexText, }; vertexNode = addHtmlLabel(svg, node).node(); vertexNode.parentNode.removeChild(vertexNode); @@ -143,11 +140,13 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) { default: _shape = 'rect'; } + const labelText = await renderKatex(vertexText, getConfig()); + // Add the node g.setNode(vertex.id, { labelStyle: styles.labelStyle, shape: _shape, - labelText: vertexText, + labelText, labelType: vertex.labelType, rx: radious, ry: radious, @@ -170,7 +169,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) { labelStyle: styles.labelStyle, labelType: vertex.labelType, shape: _shape, - labelText: vertexText, + labelText, rx: radious, ry: radious, class: classStr, @@ -183,7 +182,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) { props: vertex.props, padding: getConfig().flowchart.padding, }); - }); + } }; /** @@ -193,7 +192,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) { * @param {object} g The graph object * @param diagObj */ -export const addEdges = function (edges, g, diagObj) { +export const addEdges = async function (edges, g, diagObj) { log.info('abc78 edges = ', edges); let cnt = 0; let linkIdCnt = {}; @@ -207,7 +206,7 @@ export const addEdges = function (edges, g, diagObj) { defaultLabelStyle = defaultStyles.labelStyle; } - edges.forEach(function (edge) { + for (const edge of edges) { cnt++; // Identify Link @@ -315,9 +314,8 @@ export const addEdges = function (edges, g, diagObj) { edgeData.arrowheadStyle = 'fill: #333'; edgeData.labelpos = 'c'; } - edgeData.labelType = edge.labelType; - edgeData.label = edge.text.replace(common.lineBreakRegex, '\n'); + edgeData.label = await renderKatex(edge.text.replace(common.lineBreakRegex, '\n'), getConfig()); if (edge.style === undefined) { edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; @@ -330,7 +328,7 @@ export const addEdges = function (edges, g, diagObj) { // Add the edge to the graph g.setEdge(edge.start, edge.end, edgeData, cnt); - }); + } }; /** @@ -427,8 +425,8 @@ export const draw = async function (text, id, _version, diagObj) { g.setParent(subG.nodes[j], subG.id); } } - addVertices(vert, g, id, root, doc, diagObj); - addEdges(edges, g, diagObj); + await addVertices(vert, g, id, root, doc, diagObj); + await addEdges(edges, g, diagObj); // Add custom shapes // flowChartShapes.addToRenderV2(addShape); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js index 4b7fe272d6..00f9ef8512 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js @@ -15,7 +15,7 @@ describe('when using mermaid and ', function () { flowDb.clear(); flowDb.setGen('gen-2'); }); - it('should handle edges with text', () => { + it('should handle edges with text', async () => { parser.parse('graph TD;A-->|text ex|B;'); flowDb.getVertices(); const edges = flowDb.getEdges(); @@ -29,7 +29,7 @@ describe('when using mermaid and ', function () { }, }; - flowRenderer.addEdges(edges, mockG, diag); + await flowRenderer.addEdges(edges, mockG, diag); }); it('should handle edges without text', async function () { @@ -45,10 +45,10 @@ describe('when using mermaid and ', function () { }, }; - flowRenderer.addEdges(edges, mockG, diag); + await flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle open-ended edges', () => { + it('should handle open-ended edges', async () => { parser.parse('graph TD;A---B;'); flowDb.getVertices(); const edges = flowDb.getEdges(); @@ -61,10 +61,10 @@ describe('when using mermaid and ', function () { }, }; - flowRenderer.addEdges(edges, mockG, diag); + await flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle edges with styles defined', () => { + it('should handle edges with styles defined', async () => { parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); flowDb.getVertices(); const edges = flowDb.getEdges(); @@ -78,9 +78,9 @@ describe('when using mermaid and ', function () { }, }; - flowRenderer.addEdges(edges, mockG, diag); + await flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle edges with interpolation defined', () => { + it('should handle edges with interpolation defined', async () => { parser.parse('graph TD;A---B; linkStyle 0 interpolate basis'); flowDb.getVertices(); const edges = flowDb.getEdges(); @@ -94,9 +94,9 @@ describe('when using mermaid and ', function () { }, }; - flowRenderer.addEdges(edges, mockG, diag); + await flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle edges with text and styles defined', () => { + it('should handle edges with text and styles defined', async () => { parser.parse('graph TD;A---|the text|B; linkStyle 0 stroke:val1,stroke-width:val2;'); flowDb.getVertices(); const edges = flowDb.getEdges(); @@ -111,10 +111,10 @@ describe('when using mermaid and ', function () { }, }; - flowRenderer.addEdges(edges, mockG, diag); + await flowRenderer.addEdges(edges, mockG, diag); }); - it('should set fill to "none" by default when handling edges', () => { + it('should set fill to "none" by default when handling edges', async () => { parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); flowDb.getVertices(); const edges = flowDb.getEdges(); @@ -128,10 +128,10 @@ describe('when using mermaid and ', function () { }, }; - flowRenderer.addEdges(edges, mockG, diag); + await flowRenderer.addEdges(edges, mockG, diag); }); - it('should not set fill to none if fill is set in linkStyle', () => { + it('should not set fill to none if fill is set in linkStyle', async () => { parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2,fill:blue;'); flowDb.getVertices(); const edges = flowDb.getEdges(); @@ -144,7 +144,7 @@ describe('when using mermaid and ', function () { }, }; - flowRenderer.addEdges(edges, mockG, diag); + await flowRenderer.addEdges(edges, mockG, diag); }); }); }); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js index 142e455563..5f99f0ad61 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js @@ -5,7 +5,7 @@ import { render as Render } from 'dagre-d3-es'; import { applyStyle } from 'dagre-d3-es/src/dagre-js/util.js'; import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js'; import { log } from '../../logger.js'; -import common, { evaluate } from '../common/common.js'; +import common, { evaluate, renderKatex } from '../common/common.js'; import { interpolateToCurve, getStylesFromArray } from '../../utils.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import flowChartShapes from './flowChartShapes.js'; @@ -28,13 +28,13 @@ export const setConf = function (cnf) { * @param _doc * @param diagObj */ -export const addVertices = function (vert, g, svgId, root, _doc, diagObj) { +export const addVertices = async function (vert, g, svgId, root, _doc, diagObj) { const svg = !root ? select(`[id="${svgId}"]`) : root.select(`[id="${svgId}"]`); const doc = !_doc ? document : _doc; const keys = Object.keys(vert); // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - keys.forEach(function (id) { + for (const id of keys) { const vertex = vert[id]; /** @@ -57,9 +57,12 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) { if (evaluate(getConfig().flowchart.htmlLabels)) { // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? const node = { - label: vertexText.replace( - /fa[blrs]?:fa-[\w-]+/g, - (s) => `` + label: await renderKatex( + vertexText.replace( + /fa[blrs]?:fa-[\w-]+/g, + (s) => `` + ), + getConfig() ), }; vertexNode = addHtmlLabel(svg, node).node(); @@ -150,7 +153,7 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) { style: styles.style, id: diagObj.db.lookUpDomId(vertex.id), }); - }); + } }; /** @@ -160,7 +163,7 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) { * @param {object} g The graph object * @param diagObj */ -export const addEdges = function (edges, g, diagObj) { +export const addEdges = async function (edges, g, diagObj) { let cnt = 0; let defaultStyle; @@ -172,7 +175,7 @@ export const addEdges = function (edges, g, diagObj) { defaultLabelStyle = defaultStyles.labelStyle; } - edges.forEach(function (edge) { + for (const edge of edges) { cnt++; // Identify Link @@ -239,9 +242,12 @@ export const addEdges = function (edges, g, diagObj) { edgeData.labelType = 'html'; edgeData.label = `${edge.text.replace( - /fa[blrs]?:fa-[\w-]+/g, - (s) => `` + }">${await renderKatex( + edge.text.replace( + /fa[blrs]?:fa-[\w-]+/g, + (s) => `` + ), + getConfig() )}`; } else { edgeData.labelType = 'text'; @@ -261,7 +267,7 @@ export const addEdges = function (edges, g, diagObj) { // Add the edge to the graph g.setEdge(diagObj.db.lookUpDomId(edge.start), diagObj.db.lookUpDomId(edge.end), edgeData, cnt); - }); + } }; /** @@ -284,7 +290,7 @@ export const getClasses = function (text, diagObj) { * @param _version * @param diagObj */ -export const draw = function (text, id, _version, diagObj) { +export const draw = async function (text, id, _version, diagObj) { log.info('Drawing flowchart'); const { securityLevel, flowchart: conf } = getConfig(); let sandboxElement; @@ -350,8 +356,8 @@ export const draw = function (text, id, _version, diagObj) { g.setParent(diagObj.db.lookUpDomId(subG.nodes[j]), diagObj.db.lookUpDomId(subG.id)); } } - addVertices(vert, g, id, root, doc, diagObj); - addEdges(edges, g, diagObj); + await addVertices(vert, g, id, root, doc, diagObj); + await addEdges(edges, g, diagObj); // Create the renderer const render = new Render(); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.spec.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.spec.js index 5fb2307e53..68e13a305b 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.spec.js @@ -27,7 +27,7 @@ describe('the flowchart renderer', function () { ['cylinder', 'cylinder'], ['group', 'rect'], ].forEach(function ([type, expectedShape, expectedRadios = 0]) { - it(`should add the correct shaped node to the graph for vertex type ${type}`, function () { + it(`should add the correct shaped node to the graph for vertex type ${type}`, async function () { const fakeDiag = { db: { lookUpDomId: () => { @@ -41,7 +41,7 @@ describe('the flowchart renderer', function () { addedNodes.push([id, object]); }, }; - addVertices( + await addVertices( { v1: { type, @@ -70,7 +70,7 @@ describe('the flowchart renderer', function () { ['Multi
Line', 'Multi
Line', 'Multi
Line', 'MultiLine'].forEach(function ( labelText ) { - it('should handle multiline texts with different line breaks', function () { + it('should handle multiline texts with different line breaks', async function () { const addedNodes = []; const fakeDiag = { db: { @@ -84,7 +84,7 @@ describe('the flowchart renderer', function () { addedNodes.push([id, object]); }, }; - addVertices( + await addVertices( { v1: { type: 'rect', @@ -121,7 +121,7 @@ describe('the flowchart renderer', function () { 'color:#ccc;text-align:center;', ], ].forEach(function ([style, expectedStyle, expectedLabelStyle]) { - it(`should add the styles to style and/or labelStyle for style ${style}`, function () { + it(`should add the styles to style and/or labelStyle for style ${style}`, async function () { const addedNodes = []; const fakeDiag = { db: { @@ -135,7 +135,7 @@ describe('the flowchart renderer', function () { addedNodes.push([id, object]); }, }; - addVertices( + await addVertices( { v1: { type: 'rect', @@ -160,7 +160,7 @@ describe('the flowchart renderer', function () { }); }); - it(`should add default class to all nodes which do not have another class assigned`, function () { + it(`should add default class to all nodes which do not have another class assigned`, async function () { const addedNodes = []; const mockG = { setNode: function (id, object) { @@ -174,7 +174,7 @@ describe('the flowchart renderer', function () { }, }, }; - addVertices( + await addVertices( { v1: { type: 'rect', @@ -206,7 +206,7 @@ describe('the flowchart renderer', function () { }); describe('when adding edges to a graph', function () { - it('should handle multiline texts and set centered label position', function () { + it('should handle multiline texts and set centered label position', async function () { const addedEdges = []; const fakeDiag = { db: { @@ -220,7 +220,7 @@ describe('the flowchart renderer', function () { addedEdges.push(data); }, }; - addEdges( + await addEdges( [ { text: 'Multi
Line' }, { text: 'Multi
Line' }, @@ -251,7 +251,7 @@ describe('the flowchart renderer', function () { 'fill:red;', ], ].forEach(function ([style, expectedStyle, expectedLabelStyle]) { - it(`should add the styles to style and/or labelStyle for style ${style}`, function () { + it(`should add the styles to style and/or labelStyle for style ${style}`, async function () { const addedEdges = []; const fakeDiag = { db: { @@ -265,7 +265,7 @@ describe('the flowchart renderer', function () { addedEdges.push(data); }, }; - addEdges([{ style: style, text: 'styling' }], mockG, fakeDiag); + await addEdges([{ style: style, text: 'styling' }], mockG, fakeDiag); expect(addedEdges).toHaveLength(1); expect(addedEdges[0]).toHaveProperty('style', expectedStyle); diff --git a/packages/mermaid/src/diagrams/flowchart/styles.ts b/packages/mermaid/src/diagrams/flowchart/styles.ts index d0c3c77c6c..b30c0b9bc7 100644 --- a/packages/mermaid/src/diagrams/flowchart/styles.ts +++ b/packages/mermaid/src/diagrams/flowchart/styles.ts @@ -66,6 +66,12 @@ const getStyles = (options: FlowChartStyleOptions) => // text-anchor: start; // } + .node .katex path { + fill: #000; + stroke: #000; + stroke-width: 1px; + } + .node .label { text-align: center; } diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 8a7e2281cb..c8dab58682 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -209,7 +209,7 @@ Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers expect(diagram.db.showSequenceNumbers()).toBe(false); }); it('should show sequence numbers when autonumber is enabled', async () => { @@ -221,7 +221,7 @@ Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers expect(diagram.db.showSequenceNumbers()).toBe(true); }); @@ -1648,7 +1648,7 @@ participant Alice`; // mermaidAPI.reinitialize({ sequence: { textPlacement: textPlacement } }); await mermaidAPI.parse(str); // diagram.renderer.setConf(mermaidAPI.getConfig().sequence); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1679,7 +1679,7 @@ Note over Alice: Alice thinks expect(mermaidAPI.getConfig().sequence.mirrorActors).toBeFalsy(); await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1695,7 +1695,7 @@ participant Alice Note left of Alice: Alice thinks`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(-(conf.width / 2) - conf.actorMargin / 2); @@ -1711,7 +1711,7 @@ participant Alice Note right of Alice: Alice thinks`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1726,7 +1726,7 @@ sequenceDiagram Alice->Bob: Hello Bob, how are you?`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1744,7 +1744,7 @@ end Alice->Bob: Hello Bob, how are you?`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1759,7 +1759,7 @@ sequenceDiagram Alice->Bob: Hello Bob, how are you?`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); const mermaid = mermaidAPI.getConfig(); @@ -1779,7 +1779,7 @@ wrap Alice->Bob: Hello Bob, how are you?`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const msgs = diagram.db.getMessages(); const { bounds, models } = diagram.renderer.bounds.getBounds(); @@ -1800,7 +1800,7 @@ Note over Bob,Alice: Looks back `; // mermaidAPI.initialize({logLevel:0}) await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1815,7 +1815,7 @@ Alice->Bob: Hello Bob, how are you? Bob->Alice: Fine!`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1831,7 +1831,7 @@ Note right of Bob: Bob thinks Bob->Alice: Fine!`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1850,7 +1850,7 @@ Note left of Alice: Bob thinks Bob->Alice: Fine!`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(-(conf.width / 2) - conf.actorMargin / 2); @@ -1867,7 +1867,7 @@ Note left of Alice: Bob thinks Bob->>Alice: Fine!`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); const msgs = diagram.db.getMessages(); @@ -1888,7 +1888,7 @@ Note left of Alice: Bob thinks Bob->>Alice: Fine!`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); const msgs = diagram.db.getMessages(); @@ -1911,7 +1911,7 @@ Note left of Alice: Bob thinks Bob->>Alice: Fine!`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); const msgs = diagram.db.getMessages(); @@ -1933,7 +1933,7 @@ Note left of Alice: Bob thinks Bob->>Alice: Fine!`; // mermaidAPI.initialize({ logLevel: 0 }); await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); const msgs = diagram.db.getMessages(); @@ -1957,7 +1957,7 @@ loop Cheers Bob->Alice: Fine! end`; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); @@ -1975,7 +1975,7 @@ end`; end `; await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); expect(bounds.starty).toBe(0); @@ -2022,7 +2022,7 @@ sequenceDiagram participant Alice`; diagram.renderer.bounds.init(); await mermaidAPI.parse(str); - diagram.renderer.draw(str, 'tst', '1.2.3', diagram); + await diagram.renderer.draw(str, 'tst', '1.2.3', diagram); const { bounds, models } = diagram.renderer.bounds.getBounds(); expect(bounds.startx).toBe(0); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index b8962395ee..98a14aac74 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -1,8 +1,8 @@ // @ts-nocheck TODO: fix file import { select } from 'd3'; -import svgDraw, { ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js'; +import svgDraw, { drawKatex, ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js'; import { log } from '../../logger.js'; -import common from '../common/common.js'; +import common, { calculateMathMLDimensions, hasKatex } from '../common/common.js'; import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import assignWithDepth from '../../assignWithDepth.js'; @@ -237,7 +237,7 @@ interface NoteModel { * @param elem - The diagram to draw to. * @param noteModel - Note model options. */ -const drawNote = function (elem: any, noteModel: NoteModel) { +const drawNote = async function (elem: any, noteModel: NoteModel) { bounds.bumpVerticalPos(conf.boxMargin); noteModel.height = conf.boxMargin; noteModel.starty = bounds.getVerticalPos(); @@ -263,7 +263,7 @@ const drawNote = function (elem: any, noteModel: NoteModel) { textObj.textMargin = conf.noteMargin; textObj.valign = 'center'; - const textElem = drawText(g, textObj); + const textElem = hasKatex(textObj.text) ? await drawKatex(g, textObj) : drawText(g, textObj); const textHeight = Math.round( textElem @@ -311,15 +311,20 @@ const actorFont = (cnf) => { * @param msgModel - The model containing fields describing a message * @returns `lineStartY` - The Y coordinate at which the message line starts */ -function boundMessage(_diagram, msgModel): number { +async function boundMessage(_diagram, msgModel): Promise { bounds.bumpVerticalPos(10); const { startx, stopx, message } = msgModel; const lines = common.splitBreaks(message).length; - const textDims = utils.calculateTextDimensions(message, messageFont(conf)); - const lineHeight = textDims.height / lines; - msgModel.height += lineHeight; - - bounds.bumpVerticalPos(lineHeight); + const isKatexMsg = hasKatex(message); + const textDims = isKatexMsg + ? await calculateMathMLDimensions(message, getConfig()) + : utils.calculateTextDimensions(message, messageFont(conf)); + + if (!isKatexMsg) { + const lineHeight = textDims.height / lines; + msgModel.height += lineHeight; + bounds.bumpVerticalPos(lineHeight); + } let lineStartY; let totalOffset = textDims.height - 10; @@ -360,7 +365,7 @@ function boundMessage(_diagram, msgModel): number { * @param lineStartY - The Y coordinate at which the message line starts * @param diagObj - The diagram object. */ -const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { +const drawMessage = async function (diagram, msgModel, lineStartY: number, diagObj: Diagram) { const { startx, stopx, starty, message, type, sequenceIndex, sequenceVisible } = msgModel; const textDims = utils.calculateTextDimensions(message, messageFont(conf)); const textObj = svgDrawCommon.getTextObj(); @@ -378,7 +383,9 @@ const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Di textObj.textMargin = conf.wrapPadding; textObj.tspan = false; - drawText(diagram, textObj); + hasKatex(textObj.text) + ? await drawKatex(diagram, textObj, { startx, stopx, starty: lineStartY }) + : drawText(diagram, textObj); const textWidth = textDims.width; @@ -478,7 +485,7 @@ const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Di } }; -const addActorRenderingData = function ( +const addActorRenderingData = async function ( diagram, actors, createdActors, @@ -548,12 +555,12 @@ const addActorRenderingData = function ( bounds.bumpVerticalPos(maxHeight); }; -export const drawActors = function (diagram, actors, actorKeys, isFooter) { +export const drawActors = async function (diagram, actors, actorKeys, isFooter) { if (!isFooter) { for (const actorKey of actorKeys) { const actor = actors[actorKey]; // Draw the box with the attached line - svgDraw.drawActor(diagram, actor, conf, false); + await svgDraw.drawActor(diagram, actor, conf, false); } } else { let maxHeight = 0; @@ -563,7 +570,7 @@ export const drawActors = function (diagram, actors, actorKeys, isFooter) { if (!actor.stopy) { actor.stopy = bounds.getVerticalPos(); } - const height = svgDraw.drawActor(diagram, actor, conf, true); + const height = await svgDraw.drawActor(diagram, actor, conf, true); maxHeight = common.getMax(maxHeight, height); } bounds.bumpVerticalPos(maxHeight + conf.boxMargin); @@ -746,7 +753,7 @@ function adjustCreatedDestroyedData( * @param _version - Mermaid version from package.json * @param diagObj - A standard diagram containing the db and the text and type etc of the diagram */ -export const draw = function (_text: string, id: string, _version: string, diagObj: Diagram) { +export const draw = async function (_text: string, id: string, _version: string, diagObj: Diagram) { const { securityLevel, sequence } = getConfig(); conf = sequence; // Handle root and Document for when rendering in sandbox mode @@ -776,8 +783,8 @@ export const draw = function (_text: string, id: string, _version: string, diagO const title = diagObj.db.getDiagramTitle(); const hasBoxes = diagObj.db.hasAtLeastOneBox(); const hasBoxTitles = diagObj.db.hasAtLeastOneBoxWithTitle(); - const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages, diagObj); - conf.height = calculateActorMargins(actors, maxMessageWidthPerActor, boxes); + const maxMessageWidthPerActor = await getMaxMessageWidthPerActor(actors, messages, diagObj); + conf.height = await calculateActorMargins(actors, maxMessageWidthPerActor, boxes); svgDraw.insertComputerIcon(diagram); svgDraw.insertDatabaseIcon(diagram); @@ -799,8 +806,8 @@ export const draw = function (_text: string, id: string, _version: string, diagO actorKeys = actorKeys.filter((actorKey) => newActors.has(actorKey)); } - addActorRenderingData(diagram, actors, createdActors, actorKeys, 0, messages, false); - const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj); + await addActorRenderingData(diagram, actors, createdActors, actorKeys, 0, messages, false); + const loopWidths = await calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj); // The arrow head definition is attached to the svg once svgDraw.insertArrowHead(diagram); @@ -834,14 +841,15 @@ export const draw = function (_text: string, id: string, _version: string, diagO let sequenceIndexStep = 1; const messagesToDraw = []; const backgrounds = []; - messages.forEach(function (msg, index) { + let index = 0; + for (const msg of messages) { let loopModel, noteModel, msgModel; switch (msg.type) { case diagObj.db.LINETYPE.NOTE: bounds.resetVerticalPos(); noteModel = msg.noteModel; - drawNote(diagram, noteModel); + await drawNote(diagram, noteModel); break; case diagObj.db.LINETYPE.ACTIVE_START: bounds.newActivation(msg, diagram, actors); @@ -860,7 +868,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.LOOP_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'loop', conf); + await svgDraw.drawLoop(diagram, loopModel, 'loop', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -886,7 +894,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.OPT_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'opt', conf); + await svgDraw.drawLoop(diagram, loopModel, 'opt', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -910,7 +918,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.ALT_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'alt', conf); + await svgDraw.drawLoop(diagram, loopModel, 'alt', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -936,7 +944,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.PAR_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'par', conf); + await svgDraw.drawLoop(diagram, loopModel, 'par', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -969,7 +977,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.CRITICAL_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'critical', conf); + await svgDraw.drawLoop(diagram, loopModel, 'critical', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -984,7 +992,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO break; case diagObj.db.LINETYPE.BREAK_END: loopModel = bounds.endLoop(); - svgDraw.drawLoop(diagram, loopModel, 'break', conf); + await svgDraw.drawLoop(diagram, loopModel, 'break', conf); bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos()); bounds.models.addLoop(loopModel); break; @@ -994,7 +1002,7 @@ export const draw = function (_text: string, id: string, _version: string, diagO msgModel.starty = bounds.getVerticalPos(); msgModel.sequenceIndex = sequenceIndex; msgModel.sequenceVisible = diagObj.db.showSequenceNumbers(); - const lineStartY = boundMessage(diagram, msgModel); + const lineStartY = await boundMessage(diagram, msgModel); adjustCreatedDestroyedData( msg, msgModel, @@ -1026,20 +1034,23 @@ export const draw = function (_text: string, id: string, _version: string, diagO ) { sequenceIndex = sequenceIndex + sequenceIndexStep; } - }); + index++; + } log.debug('createdActors', createdActors); log.debug('destroyedActors', destroyedActors); + await drawActors(diagram, actors, actorKeys, false); - drawActors(diagram, actors, actorKeys, false); - messagesToDraw.forEach((e) => drawMessage(diagram, e.messageModel, e.lineStartY, diagObj)); + for (const e of messagesToDraw) { + await drawMessage(diagram, e.messageModel, e.lineStartY, diagObj); + } if (conf.mirrorActors) { - drawActors(diagram, actors, actorKeys, true); + await drawActors(diagram, actors, actorKeys, true); } backgrounds.forEach((e) => svgDraw.drawBackgroundRect(diagram, e)); fixLifeLineHeights(diagram, actors, actorKeys, conf); - bounds.models.boxes.forEach(function (box) { + for (const box of bounds.models.boxes) { box.height = bounds.getVerticalPos() - box.y; bounds.insert(box.x, box.y, box.x + box.width, box.height); box.startx = box.x; @@ -1047,8 +1058,8 @@ export const draw = function (_text: string, id: string, _version: string, diagO box.stopx = box.startx + box.width; box.stopy = box.starty + box.height; box.stroke = 'rgb(0,0,0, 0.5)'; - svgDraw.drawBox(diagram, box, conf); - }); + await svgDraw.drawBox(diagram, box, conf); + } if (hasBoxes) { bounds.bumpVerticalPos(conf.boxMargin); @@ -1114,25 +1125,25 @@ export const draw = function (_text: string, id: string, _version: string, diagO * @param diagObj - The diagram object. * @returns The max message width of each actor. */ -function getMaxMessageWidthPerActor( +async function getMaxMessageWidthPerActor( actors: { [id: string]: any }, messages: any[], diagObj: Diagram -): { [id: string]: number } { +): Promise<{ [id: string]: number }> { const maxMessageWidthPerActor = {}; - messages.forEach(function (msg) { + for (const msg of messages) { if (actors[msg.to] && actors[msg.from]) { const actor = actors[msg.to]; // If this is the first actor, and the message is left of it, no need to calculate the margin if (msg.placement === diagObj.db.PLACEMENT.LEFTOF && !actor.prevActor) { - return; + continue; } // If this is the last actor, and the message is right of it, no need to calculate the margin if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF && !actor.nextActor) { - return; + continue; } const isNote = msg.placement !== undefined; @@ -1142,7 +1153,9 @@ function getMaxMessageWidthPerActor( const wrappedMessage = msg.wrap ? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont) : msg.message; - const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont); + const messageDimensions = hasKatex(wrappedMessage) + ? await calculateMathMLDimensions(msg.message, getConfig()) + : utils.calculateTextDimensions(wrappedMessage, textFont); const messageWidth = messageDimensions.width + 2 * conf.wrapPadding; /* @@ -1207,7 +1220,7 @@ function getMaxMessageWidthPerActor( } } } - }); + } log.debug('maxMessageWidthPerActor:', maxMessageWidthPerActor); return maxMessageWidthPerActor; @@ -1238,13 +1251,13 @@ const getRequiredPopupWidth = function (actor) { * @param actorToMessageWidth - A map of actor key → max message width it holds * @param boxes - The boxes around the actors if any */ -function calculateActorMargins( +async function calculateActorMargins( actors: { [id: string]: any }, - actorToMessageWidth: ReturnType, + actorToMessageWidth: Awaited>, boxes ) { let maxHeight = 0; - Object.keys(actors).forEach((prop) => { + for (const prop of Object.keys(actors)) { const actor = actors[prop]; if (actor.wrap) { actor.description = utils.wrapLabel( @@ -1253,14 +1266,17 @@ function calculateActorMargins( actorFont(conf) ); } - const actDims = utils.calculateTextDimensions(actor.description, actorFont(conf)); + const actDims = hasKatex(actor.description) + ? await calculateMathMLDimensions(actor.description, getConfig()) + : utils.calculateTextDimensions(actor.description, actorFont(conf)); + actor.width = actor.wrap ? conf.width : common.getMax(conf.width, actDims.width + 2 * conf.wrapPadding); actor.height = actor.wrap ? common.getMax(actDims.height, conf.height) : conf.height; maxHeight = common.getMax(maxHeight, actor.height); - }); + } for (const actorKey in actorToMessageWidth) { const actor = actors[actorKey]; @@ -1311,15 +1327,17 @@ function calculateActorMargins( return common.getMax(maxHeight, conf.height); } -const buildNoteModel = function (msg, actors, diagObj) { +const buildNoteModel = async function (msg, actors, diagObj) { const startx = actors[msg.from].x; const stopx = actors[msg.to].x; const shouldWrap = msg.wrap && msg.message; - let textDimensions = utils.calculateTextDimensions( - shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message, - noteFont(conf) - ); + let textDimensions: { width: number; height: number; lineHeight?: number } = hasKatex(msg.message) + ? await calculateMathMLDimensions(msg.message, getConfig()) + : utils.calculateTextDimensions( + shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message, + noteFont(conf) + ); const noteModel = { width: shouldWrap ? conf.width @@ -1477,12 +1495,12 @@ const buildMessageModel = function (msg, actors, diagObj) { }; }; -const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagObj) { +const calculateLoopBounds = async function (messages, actors, _maxWidthPerActor, diagObj) { const loops = {}; const stack = []; let current, noteModel, msgModel; - messages.forEach(function (msg) { + for (const msg of messages) { msg.id = utils.random({ length: 10 }); switch (msg.type) { case diagObj.db.LINETYPE.LOOP_START: @@ -1545,7 +1563,7 @@ const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagO } const isNote = msg.placement !== undefined; if (isNote) { - noteModel = buildNoteModel(msg, actors, diagObj); + noteModel = await buildNoteModel(msg, actors, diagObj); msg.noteModel = noteModel; stack.forEach((stk) => { current = stk; @@ -1584,7 +1602,7 @@ const calculateLoopBounds = function (messages, actors, _maxWidthPerActor, diagO }); } } - }); + } bounds.activations = []; log.debug('Loop type widths:', loops); return loops; diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index e2f1ffa6a6..12767f8898 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -1,8 +1,9 @@ -import common from '../common/common.js'; +import common, { calculateMathMLDimensions, hasKatex, renderKatex } from '../common/common.js'; import * as svgDrawCommon from '../common/svgDrawCommon.js'; import { addFunction } from '../../interactionDb.js'; import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js'; import { sanitizeUrl } from '@braintree/sanitize-url'; +import * as configApi from '../../config.js'; export const ACTOR_TYPE_WIDTH = 18 * 2; const TOP_ACTOR_CLASS = 'actor-top'; @@ -83,6 +84,47 @@ const popupMenuToggle = function (popid) { ); }; +export const drawKatex = async function (elem, textData, msgModel = null) { + let textElem = elem.append('foreignObject'); + const lines = await renderKatex(textData.text, configApi.getConfig()); + + const divElem = textElem + .append('xhtml:div') + .attr('style', 'width: fit-content;') + .attr('xmlns', 'http://www.w3.org/1999/xhtml') + .html(lines); + const dim = divElem.node().getBoundingClientRect(); + + textElem.attr('height', Math.round(dim.height)).attr('width', Math.round(dim.width)); + + if (textData.class === 'noteText') { + const rectElem = elem.node().firstChild; + + rectElem.setAttribute('height', dim.height + 2 * textData.textMargin); + const rectDim = rectElem.getBBox(); + + textElem + .attr('x', Math.round(rectDim.x + rectDim.width / 2 - dim.width / 2)) + .attr('y', Math.round(rectDim.y + rectDim.height / 2 - dim.height / 2)); + } else if (msgModel) { + let { startx, stopx, starty } = msgModel; + if (startx > stopx) { + const temp = startx; + startx = stopx; + stopx = temp; + } + + textElem.attr('x', Math.round(startx + Math.abs(startx - stopx) / 2 - dim.width / 2)); + if (textData.class === 'loopText') { + textElem.attr('y', Math.round(starty)); + } else { + textElem.attr('y', Math.round(starty - dim.height)); + } + } + + return [textElem]; +}; + export const drawText = function (elem, textData) { let prevTextHeight = 0; let textHeight = 0; @@ -282,7 +324,7 @@ export const fixLifeLineHeights = (diagram, actors, actorKeys, conf) => { * @param {any} conf - DrawText implementation discriminator object * @param {boolean} isFooter - If the actor is the footer one */ -const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { +const drawActorTypeParticipant = async function (elem, actor, conf, isFooter) { const actorY = isFooter ? actor.stopy : actor.starty; const center = actor.x + actor.width / 2; const centerY = actorY + 5; @@ -345,7 +387,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { } } - _drawTextCandidateFunc(conf)( + await _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, g, rect.x, @@ -366,7 +408,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) { return height; }; -const drawActorTypeActor = function (elem, actor, conf, isFooter) { +const drawActorTypeActor = async function (elem, actor, conf, isFooter) { const actorY = isFooter ? actor.stopy : actor.starty; const center = actor.x + actor.width / 2; const centerY = actorY + 80; @@ -446,7 +488,7 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) { const bounds = actElem.node().getBBox(); actor.height = bounds.height; - _drawTextCandidateFunc(conf)( + await _drawTextCandidateFunc(conf, hasKatex(actor.description))( actor.description, actElem, rect.x, @@ -460,21 +502,21 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) { return actor.height; }; -export const drawActor = function (elem, actor, conf, isFooter) { +export const drawActor = async function (elem, actor, conf, isFooter) { switch (actor.type) { case 'actor': - return drawActorTypeActor(elem, actor, conf, isFooter); + return await drawActorTypeActor(elem, actor, conf, isFooter); case 'participant': - return drawActorTypeParticipant(elem, actor, conf, isFooter); + return await drawActorTypeParticipant(elem, actor, conf, isFooter); } }; -export const drawBox = function (elem, box, conf) { +export const drawBox = async function (elem, box, conf) { const boxplustextGroup = elem.append('g'); const g = boxplustextGroup; drawBackgroundRect(g, box); if (box.name) { - _drawTextCandidateFunc(conf)( + await _drawTextCandidateFunc(conf)( box.name, g, box.x, @@ -521,7 +563,7 @@ export const drawActivation = function (elem, bounds, verticalPos, conf, actorAc * @param {any} conf - Diagram configuration * @returns {any} */ -export const drawLoop = function (elem, loopModel, labelText, conf) { +export const drawLoop = async function (elem, loopModel, labelText, conf) { const { boxMargin, boxTextMargin, @@ -583,10 +625,10 @@ export const drawLoop = function (elem, loopModel, labelText, conf) { txt.fontWeight = fontWeight; txt.wrap = true; - let textElem = drawText(g, txt); + let textElem = hasKatex(txt.text) ? await drawKatex(g, txt, loopModel) : drawText(g, txt); if (loopModel.sectionTitles !== undefined) { - loopModel.sectionTitles.forEach(function (item, idx) { + for (const [idx, item] of Object.entries(loopModel.sectionTitles)) { if (item.message) { txt.text = item.message; txt.x = loopModel.startx + (loopModel.stopx - loopModel.startx) / 2; @@ -599,7 +641,13 @@ export const drawLoop = function (elem, loopModel, labelText, conf) { txt.fontSize = fontSize; txt.fontWeight = fontWeight; txt.wrap = loopModel.wrap; - textElem = drawText(g, txt); + + if (hasKatex(txt.text)) { + loopModel.starty = loopModel.sections[idx].y; + await drawKatex(g, txt, loopModel); + } else { + drawText(g, txt); + } let sectionHeight = Math.round( textElem .map((te) => (te._groups || te)[0][0].getBBox().height) @@ -607,7 +655,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) { ); loopModel.sections[idx].height += sectionHeight - (boxMargin + boxTextMargin); } - }); + } } loopModel.height = Math.round(loopModel.stopy - loopModel.starty); @@ -884,6 +932,41 @@ const _drawTextCandidateFunc = (function () { _setTextAttrs(text, textAttrs); } + /** + * + * @param content + * @param g + * @param x + * @param y + * @param width + * @param height + * @param textAttrs + * @param conf + */ + async function byKatex(content, g, x, y, width, height, textAttrs, conf) { + // TODO duplicate render calls, optimize + + const dim = await calculateMathMLDimensions(content, configApi.getConfig()); + const s = g.append('switch'); + const f = s + .append('foreignObject') + .attr('x', x + width / 2 - dim.width / 2) + .attr('y', y + height / 2 - dim.height / 2) + .attr('width', dim.width) + .attr('height', dim.height); + + const text = f.append('xhtml:div').style('height', '100%').style('width', '100%'); + + text + .append('div') + .style('text-align', 'center') + .style('vertical-align', 'middle') + .html(await renderKatex(content, configApi.getConfig())); + + byTspan(content, s, x, y, width, height, textAttrs, conf); + _setTextAttrs(text, textAttrs); + } + /** * @param {any} toText * @param {any} fromTextAttrsDict @@ -896,7 +979,10 @@ const _drawTextCandidateFunc = (function () { } } - return function (conf) { + return function (conf, hasKatex = false) { + if (hasKatex) { + return byKatex; + } return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan; }; })(); diff --git a/packages/mermaid/src/docs/.vitepress/config.ts b/packages/mermaid/src/docs/.vitepress/config.ts index 7c79cc18fc..401130518b 100644 --- a/packages/mermaid/src/docs/.vitepress/config.ts +++ b/packages/mermaid/src/docs/.vitepress/config.ts @@ -172,6 +172,7 @@ function sidebarConfig() { { text: 'Mermaid Configuration Options', link: '/config/schema-docs/config' }, { text: 'Directives', link: '/config/directives' }, { text: 'Theming', link: '/config/theming' }, + { text: 'Math', link: '/config/math' }, { text: 'Accessibility', link: '/config/accessibility' }, { text: 'Mermaid CLI', link: '/config/mermaidCLI' }, { text: 'FAQ', link: '/config/faq' }, diff --git a/packages/mermaid/src/docs/config/math.md b/packages/mermaid/src/docs/config/math.md new file mode 100644 index 0000000000..d72320040e --- /dev/null +++ b/packages/mermaid/src/docs/config/math.md @@ -0,0 +1,62 @@ +# Math Configuration (v+) + +Mermaid supports rendering mathematical expressions through the [KaTeX](https://katex.org/) typesetter. + +## Usage + +To render math within a diagram, surround the mathematical expression with the `$$` delimiter. + +Note that at the moment, the only supported diagrams are below: + +### Flowcharts + +```mermaid + graph LR + A["$$x^2$$"] -->|"$$\sqrt{x+3}$$"| B("$$\frac{1}{2}$$") + A -->|"$$\overbrace{a+b+c}^{\text{note}}$$"| C("$$\pi r^2$$") + B --> D("$$x = \begin{cases} a &\text{if } b \\ c &\text{if } d \end{cases}$$") + C --> E("$$x(t)=c_1\begin{bmatrix}-\cos{t}+\sin{t}\\ 2\cos{t} \end{bmatrix}e^{2t}$$") +``` + +### Sequence + +```mermaid +sequenceDiagram + autonumber + participant 1 as $$\alpha$$ + participant 2 as $$\beta$$ + 1->>2: Solve: $$\sqrt{2+2}$$ + 2-->>1: Answer: $$2$$ + Note right of 2: $$\sqrt{2+2}=\sqrt{4}=2$$ +``` + +## Legacy Support + +By default, MathML is used for rendering mathematical expressions. If you have users on [unsupported browsers](https://caniuse.com/?search=mathml), `legacyMathML` can be set in the config to fall back to CSS rendering. Note that **you must provide KaTeX's stylesheets on your own** as they do not come bundled with Mermaid. + +Example with legacy mode enabled (the latest version of KaTeX's stylesheet can be found on their [docs](https://katex.org/docs/browser.html)): + +```html + + + + + + + + + + + + +``` diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 415af63f69..3e7fd58ec5 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -168,6 +168,14 @@ properties: items: type: string uniqueItems: false # Should be enabled, but it may be a breaking change from the old config + legacyMathML: + description: | + This option specifies if Mermaid can expect the dependent to include KaTeX stylesheets for browsers + without their own MathML implementation. If this option is disabled and MathML is not supported, the math + equations are replaced with a warning. If this option is enabled and MathML is not supported, Mermaid will + fall back to legacy rendering for KaTeX. + type: boolean + default: false deterministicIds: description: | This option controls if the generated ids of nodes in the SVG are diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08c9db3798..2a530cfb57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,9 @@ importers: elkjs: specifier: ^0.9.0 version: 0.9.1 + katex: + specifier: ^0.16.9 + version: 0.16.9 khroma: specifier: ^2.0.0 version: 2.0.0 @@ -252,7 +255,7 @@ importers: version: 9.0.0 web-worker: specifier: ^1.2.0 - version: 1.2.0 + version: 1.3.0 devDependencies: '@adobe/jsonschema2md': specifier: ^7.1.4 @@ -278,6 +281,9 @@ importers: '@types/jsdom': specifier: ^21.1.1 version: 21.1.1 + '@types/katex': + specifier: ^0.16.7 + version: 0.16.7 '@types/lodash-es': specifier: ^4.17.7 version: 4.17.7 @@ -4897,6 +4903,10 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/katex@0.16.7: + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + dev: true + /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: @@ -7436,6 +7446,11 @@ packages: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: false + /comment-json@4.2.3: resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} engines: {node: '>= 6'} @@ -11843,6 +11858,13 @@ packages: engines: {node: '>=12.20'} dev: true + /katex@0.16.9: + resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + /keyv@4.5.3: resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} dependencies: @@ -16522,8 +16544,8 @@ packages: engines: {node: '>= 8'} dev: true - /web-worker@1.2.0: - resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==} + /web-worker@1.3.0: + resolution: {integrity: sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==} dev: false /webdriver@7.31.1(typescript@5.1.6):