feat(katex): added KaTeX support to sequence diagrams

This commit is contained in:
NicolasNewman
2023-05-06 17:19:31 +09:00
parent f8a4488050
commit 4202488da0
4 changed files with 284 additions and 146 deletions

View File

@@ -16,134 +16,167 @@
<body> <body>
<h1>Sequence diagram demos</h1> <h1>Sequence diagram demos</h1>
<pre class="mermaid"> <pre class="mermaid">
sequenceDiagram sequenceDiagram
accTitle: test the accTitle accTitle: test the accTitle
accDescr: Test a description accDescr: Test a description
participant Alice participant Alice
participant Bob participant Bob
participant John as John<br />Second Line participant John as John<br />Second Line
autonumber 10 10 autonumber 10 10
rect rgb(200, 220, 100) rect rgb(200, 220, 100)
rect rgb(200, 255, 200) rect rgb(200, 255, 200)
Alice ->> Bob: Hello Bob, how are you? Alice ->> Bob: Hello Bob, how are you?
Bob-->>John: How about you John? Bob-->>John: How about you John?
end end
Bob--x Alice: I am good thanks! Bob--x Alice: I am good thanks!
Bob-x John: I am good thanks! Bob-x John: I am good thanks!
Note right of John: John thinks a long<br />long time, so long<br />that the text does<br />not fit on a row. Note right of John: John thinks a long<br />long time, so long<br />that the text does<br />not fit on a row.
Bob-->Alice: Checking with John... Bob-->Alice: Checking with John...
Note over John:wrap: John looks like he's still thinking, so Bob prods him a bit. 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<br />how you're doing Bob-x John: Hey John - we're still waiting to know<br />how you're doing
Note over John:nowrap: John's trying hard not to break his train of thought. 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?? 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<br />finally snaps out of it. Note over John: After a few more moments, John<br />finally snaps out of it.
end end
autonumber off autonumber off
alt either this alt either this
Alice->>+John: Yes Alice->>+John: Yes
John-->>-Alice: OK John-->>-Alice: OK
else or this else or this
autonumber autonumber
Alice->>John: No Alice->>John: No
else or this will happen else or this will happen
Alice->John: Maybe Alice->John: Maybe
end end
autonumber 200 autonumber 200
par this happens in parallel par this happens in parallel
Alice -->> Bob: Parallel message 1 Alice -->> Bob: Parallel message 1
and and
Alice -->> John: Parallel message 2 Alice -->> John: Parallel message 2
end end
</pre>
<hr />
<pre class="mermaid">
sequenceDiagram
accTitle: Sequence diagram title is here
accDescr: Hello friends
participant Alice
participant Bob
participant John as John<br />Second Line
rect rgb(200, 220, 100)
rect rgb(200, 255, 200)
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<br />long time, so long<br />that the text does<br />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<br />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<br />finally snaps out of it.
end
alt either this
Alice->>John: Yes
else or this
Alice->>John: No
else or this will happen
Alice->John: Maybe
end
par this happens in parallel
Alice -->> Bob: Parallel message 1
and
Alice -->> John: Parallel message 2
end
</pre> </pre>
<hr /> <hr />
<pre class="mermaid"> <pre class="mermaid">
sequenceDiagram sequenceDiagram
participant 1 as multiline<br>using #lt;br#gt; accTitle: Sequence diagram title is here
participant 2 as multiline<br />using #lt;br/#gt; accDescr: Hello friends
participant 3 as multiline<br />using #lt;br /#gt;
participant 4 as multiline<br />using #lt;br /#gt;
1->>2: multiline<br>using #lt;br#gt;
note right of 2: multiline<br>using #lt;br#gt;
2->>3: multiline<br />using #lt;br/#gt;
note right of 3: multiline<br />using #lt;br/#gt;
3->>4: multiline<br />using #lt;br /#gt;
note right of 4: multiline<br />using #lt;br /#gt;
4->>1: multiline<br />using #lt;br /#gt;
note right of 1: multiline<br />using #lt;br /#gt;
</pre>
<hr />
<pre class="mermaid"> participant Alice
sequenceDiagram participant Bob
autonumber participant John as John<br />Second Line
Alice->>John: Hello John,<br>how are you? rect rgb(200, 220, 100)
autonumber 50 10 rect rgb(200, 255, 200)
Alice->>John: John,<br />can you hear me? Alice ->> Bob: Hello Bob, how are you?
John-->>Alice: Hi Alice,<br />I can hear you! Bob-->>John: How about you John?
autonumber off end
John-->>Alice: I feel great! Bob--x Alice: I am good thanks!
</pre> Bob-x John: I am good thanks!
Note right of John: John thinks a long<br />long time, so long<br />that the text does<br />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<br />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<br />finally snaps out of it.
end
alt either this
Alice->>John: Yes
else or this
Alice->>John: No
else or this will happen
Alice->John: Maybe
end
par this happens in parallel
Alice -->> Bob: Parallel message 1
and
Alice -->> John: Parallel message 2
end
</pre>
<hr /> <hr />
<pre class="mermaid"> <pre class="mermaid">
sequenceDiagram sequenceDiagram
box lightgreen Alice & John participant 1 as multiline<br>using #lt;br#gt;
participant A participant 2 as multiline<br />using #lt;br/#gt;
participant J participant 3 as multiline<br />using #lt;br /#gt;
end participant 4 as multiline<br />using #lt;br /#gt;
box Another Group very very long description not wrapped 1->>2: multiline<br>using #lt;br#gt;
participant B note right of 2: multiline<br>using #lt;br#gt;
end 2->>3: multiline<br />using #lt;br/#gt;
A->>J: Hello John, how are you? note right of 3: multiline<br />using #lt;br/#gt;
J->>A: Great! 3->>4: multiline<br />using #lt;br /#gt;
A->>B: Hello Bob, how are you ? note right of 4: multiline<br />using #lt;br /#gt;
</pre 4->>1: multiline<br />using #lt;br /#gt;
note right of 1: multiline<br />using #lt;br /#gt;
</pre>
<hr />
<pre class="mermaid">
sequenceDiagram
autonumber
Alice->>John: Hello John,<br>how are you?
autonumber 50 10
Alice->>John: John,<br />can you hear me?
John-->>Alice: Hi Alice,<br />I can hear you!
autonumber off
John-->>Alice: I feel great!
</pre>
<hr />
<pre class="mermaid">
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 ?
</pre
> >
<hr /> <hr />
<pre class="mermaid">
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<br />using #lt;br /#gt;
note right of 1: multiline<br />$$\frac{1}{2}$$<br />3rd line
</pre>
<hr />
<pre class="mermaid">
sequenceDiagram
autonumber
participant 1 as $$\alpha$$lex
participant 2 as $$\beta$$ob
participant 3 as $$\theta$$iffany
1->>2: Hello John, does&nbsp; $$\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$$
</pre>
<hr />
<script type="module"> <script type="module">
import mermaid from './mermaid.esm.mjs'; import mermaid from './mermaid.esm.mjs';
mermaid.initialize({ mermaid.initialize({

View File

@@ -195,7 +195,8 @@ export const hasKatex = (text: string): boolean => (text.match(katexRegex)?.leng
* @returns Object containing {width, height} * @returns Object containing {width, height}
*/ */
export const calculateMathMLDimensions = (text: string, config: MermaidConfig) => { export const calculateMathMLDimensions = (text: string, config: MermaidConfig) => {
text = renderKatex(text, config).split(lineBreakRegex).map((text) => hasKatex(text) ? renderKatex(text, config) : `<div>${text}</div>`).join('');
text = renderKatex(text, config);
const divElem = document.createElement('div') const divElem = document.createElement('div')
divElem.innerHTML = text; divElem.innerHTML = text;
divElem.id = 'katex-temp'; divElem.id = 'katex-temp';
@@ -209,6 +210,13 @@ export const calculateMathMLDimensions = (text: string, config: MermaidConfig) =
return dim; return dim;
} }
// export const temp = (text: string, config: MermaidConfig) => {
// return renderKatex(text, config).split(lineBreakRegex).map((text) =>
// hasKatex(text) ?
// `<div style="display: flex;">${text}</div>` :
// `<div>${text}</div>`).join('');
// }
/** /**
* Attempts to render and return the KaTeX portion of a string with MathML * Attempts to render and return the KaTeX portion of a string with MathML
* *
@@ -218,14 +226,25 @@ export const calculateMathMLDimensions = (text: string, config: MermaidConfig) =
*/ */
export const renderKatex = (text: string, config: MermaidConfig): string => { export const renderKatex = (text: string, config: MermaidConfig): string => {
if (isMathMLSupported || (!isMathMLSupported && config.legacyMathML)) { if (isMathMLSupported || (!isMathMLSupported && config.legacyMathML)) {
return text.replace(/\$\$(.*)\$\$/g, (r, c) => return text
katex .split(lineBreakRegex)
.renderToString(c, { throwOnError: true, displayMode: true, output: isMathMLSupported ? 'mathml' : 'htmlAndMathml' }) .map((line) => hasKatex(line) ?
.replace(/\n/g, ' ') `
.replace(/<annotation.*<\/annotation>/g, '') <div style="display: flex; align-items: center; justify-content: center; white-space: nowrap;">
); ${line}
</div>
` :
`<div>${line}</div>`
)
.join('')
.replace(katexRegex, (r, c) =>
katex
.renderToString(c, { throwOnError: true, displayMode: true, output: isMathMLSupported ? 'mathml' : 'htmlAndMathml' })
.replace(/\n/g, ' ')
.replace(/<annotation.*<\/annotation>/g, '')
)
} }
return text.replace(/\$\$(.*)\$\$/g, (r, c) => 'MathML is unsupported in this environment.'); return text.replace(katexRegex, 'MathML is unsupported in this environment.');
}; };
export default { export default {

View File

@@ -1,8 +1,8 @@
// @ts-nocheck TODO: fix file // @ts-nocheck TODO: fix file
import { select, selectAll } from 'd3'; import { select, selectAll } from 'd3';
import svgDraw, { drawText, fixLifeLineHeights } from './svgDraw.js'; import svgDraw, { drawKatex, drawText, fixLifeLineHeights } from './svgDraw.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import common from '../common/common.js'; import common, { calculateMathMLDimensions, hasKatex } from '../common/common.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import assignWithDepth from '../../assignWithDepth.js'; import assignWithDepth from '../../assignWithDepth.js';
import utils from '../../utils.js'; import utils from '../../utils.js';
@@ -247,7 +247,7 @@ const drawNote = function (elem: any, noteModel: NoteModel) {
textObj.textMargin = conf.noteMargin; textObj.textMargin = conf.noteMargin;
textObj.valign = 'center'; textObj.valign = 'center';
const textElem = drawText(g, textObj); const textElem = hasKatex(textObj.text) ? drawKatex(g, textObj) : drawText(g, textObj);
const textHeight = Math.round( const textHeight = Math.round(
textElem textElem
@@ -299,11 +299,16 @@ function boundMessage(_diagram, msgModel): number {
bounds.bumpVerticalPos(10); bounds.bumpVerticalPos(10);
const { startx, stopx, message } = msgModel; const { startx, stopx, message } = msgModel;
const lines = common.splitBreaks(message).length; const lines = common.splitBreaks(message).length;
const textDims = utils.calculateTextDimensions(message, messageFont(conf)); const isKatexMsg = hasKatex(message);
const lineHeight = textDims.height / lines; const textDims = isKatexMsg ?
msgModel.height += lineHeight; calculateMathMLDimensions(message, configApi.getConfig()) :
utils.calculateTextDimensions(message, messageFont(conf));
bounds.bumpVerticalPos(lineHeight); if (!isKatexMsg) {
const lineHeight = textDims.height / lines;
msgModel.height += lineHeight;
bounds.bumpVerticalPos(lineHeight);
}
let lineStartY; let lineStartY;
let totalOffset = textDims.height - 10; let totalOffset = textDims.height - 10;
@@ -362,7 +367,7 @@ const drawMessage = function (diagram, msgModel, lineStartY: number, diagObj: Di
textObj.textMargin = conf.wrapPadding; textObj.textMargin = conf.wrapPadding;
textObj.tspan = false; textObj.tspan = false;
drawText(diagram, textObj); hasKatex(textObj.text) ? drawKatex(diagram, textObj, {startx, stopx, starty: lineStartY}) : drawText(diagram, textObj);
const textWidth = textDims.width; const textWidth = textDims.width;
@@ -1005,7 +1010,9 @@ function getMaxMessageWidthPerActor(
const wrappedMessage = msg.wrap const wrappedMessage = msg.wrap
? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont) ? utils.wrapLabel(msg.message, conf.width - 2 * conf.wrapPadding, textFont)
: msg.message; : msg.message;
const messageDimensions = utils.calculateTextDimensions(wrappedMessage, textFont); const messageDimensions = hasKatex(wrappedMessage) ?
calculateMathMLDimensions(msg.message, configApi.getConfig()) :
utils.calculateTextDimensions(wrappedMessage, textFont);
const messageWidth = messageDimensions.width + 2 * conf.wrapPadding; const messageWidth = messageDimensions.width + 2 * conf.wrapPadding;
/* /*
@@ -1116,7 +1123,10 @@ function calculateActorMargins(
actorFont(conf) actorFont(conf)
); );
} }
const actDims = utils.calculateTextDimensions(actor.description, actorFont(conf)); const actDims = hasKatex(actor.description) ?
calculateMathMLDimensions(actor.description, configApi.getConfig()) :
utils.calculateTextDimensions(actor.description, actorFont(conf));
actor.width = actor.wrap actor.width = actor.wrap
? conf.width ? conf.width
: Math.max(conf.width, actDims.width + 2 * conf.wrapPadding); : Math.max(conf.width, actDims.width + 2 * conf.wrapPadding);
@@ -1179,20 +1189,22 @@ const buildNoteModel = function (msg, actors, diagObj) {
const stopx = actors[msg.to].x; const stopx = actors[msg.to].x;
const shouldWrap = msg.wrap && msg.message; const shouldWrap = msg.wrap && msg.message;
let textDimensions = utils.calculateTextDimensions( let textDimensions: {width: number, height: number, lineHeight?: number} = hasKatex(msg.message) ?
shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message, calculateMathMLDimensions(msg.message, configApi.getConfig()) :
noteFont(conf) utils.calculateTextDimensions(
); shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message,
noteFont(conf)
);
const noteModel = { const noteModel = {
width: shouldWrap width: shouldWrap
? conf.width ? conf.width
: Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin), : Math.max(conf.width, textDimensions.width + 2 * conf.noteMargin),
height: 0, height: 0,
startx: actors[msg.from].x, startx: actors[msg.from].x,
stopx: 0, stopx: 0,
starty: 0, starty: 0,
stopy: 0, stopy: 0,
message: msg.message, message: msg.message,
}; };
if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) { if (msg.placement === diagObj.db.PLACEMENT.RIGHTOF) {
noteModel.width = shouldWrap noteModel.width = shouldWrap

View File

@@ -1,7 +1,8 @@
import common from '../common/common.js'; import common, { calculateMathMLDimensions, hasKatex, renderKatex } from '../common/common.js';
import { addFunction } from '../../interactionDb.js'; import { addFunction } from '../../interactionDb.js';
import { parseFontSize } from '../../utils.js'; import { parseFontSize } from '../../utils.js';
import { sanitizeUrl } from '@braintree/sanitize-url'; import { sanitizeUrl } from '@braintree/sanitize-url';
import * as configApi from '../../config.js';
export const drawRect = function (elem, rectData) { export const drawRect = function (elem, rectData) {
const rectElem = elem.append('rect'); const rectElem = elem.append('rect');
@@ -152,6 +153,48 @@ const popupMenuDownFunc = function (popupId) {
pu.style.display = 'none'; pu.style.display = 'none';
} }
}; };
export const drawKatex = function (elem, textData, msgModel = null) {
let textElem = elem.append('foreignObject');
const lines = 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) { export const drawText = function (elem, textData) {
let prevTextHeight = 0, let prevTextHeight = 0,
textHeight = 0; textHeight = 0;
@@ -397,7 +440,7 @@ const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
} }
} }
_drawTextCandidateFunc(conf)( _drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description, actor.description,
g, g,
rect.x, rect.x,
@@ -487,7 +530,7 @@ const drawActorTypeActor = function (elem, actor, conf, isFooter) {
const bounds = actElem.node().getBBox(); const bounds = actElem.node().getBBox();
actor.height = bounds.height; actor.height = bounds.height;
_drawTextCandidateFunc(conf)( _drawTextCandidateFunc(conf, hasKatex(actor.description))(
actor.description, actor.description,
actElem, actElem,
rect.x, rect.x,
@@ -623,7 +666,8 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
txt.fontWeight = fontWeight; txt.fontWeight = fontWeight;
txt.wrap = true; txt.wrap = true;
let textElem = drawText(g, txt);
let textElem = hasKatex(txt.text) ? drawKatex(g, txt, loopModel) : drawText(g, txt);
if (loopModel.sectionTitles !== undefined) { if (loopModel.sectionTitles !== undefined) {
loopModel.sectionTitles.forEach(function (item, idx) { loopModel.sectionTitles.forEach(function (item, idx) {
@@ -639,7 +683,13 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
txt.fontSize = fontSize; txt.fontSize = fontSize;
txt.fontWeight = fontWeight; txt.fontWeight = fontWeight;
txt.wrap = loopModel.wrap; txt.wrap = loopModel.wrap;
textElem = drawText(g, txt);
if (hasKatex(txt.text)) {
loopModel.starty = loopModel.sections[idx].y;
drawKatex(g, txt, loopModel);
} else {
drawText(g, txt);
}
let sectionHeight = Math.round( let sectionHeight = Math.round(
textElem textElem
.map((te) => (te._groups || te)[0][0].getBBox().height) .map((te) => (te._groups || te)[0][0].getBBox().height)
@@ -930,6 +980,29 @@ const _drawTextCandidateFunc = (function () {
_setTextAttrs(text, textAttrs); _setTextAttrs(text, textAttrs);
} }
function byKatex(content, g, x, y, width, height, textAttrs, conf) {
// TODO duplicate render calls, optimize
const dim = 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(renderKatex(content, configApi.getConfig()));
byTspan(content, s, x, y, width, height, textAttrs, conf);
_setTextAttrs(text, textAttrs);
}
/** /**
* @param {any} toText * @param {any} toText
* @param {any} fromTextAttrsDict * @param {any} fromTextAttrsDict
@@ -942,7 +1015,8 @@ 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; return conf.textPlacement === 'fo' ? byFo : conf.textPlacement === 'old' ? byText : byTspan;
}; };
})(); })();