Skip to content

Commit

Permalink
5460 - Initial Release for review
Browse files Browse the repository at this point in the history
mermaid-js#5460
This adds Markdown support for SequenceDiagram notes
Needs cleanup.
  • Loading branch information
darkedges committed Apr 13, 2024
1 parent e570863 commit 379da2b
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 15 deletions.
1 change: 1 addition & 0 deletions demos/dev/example.html
Expand Up @@ -19,6 +19,7 @@
flex: 1;
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
</head>
<body>
<pre id="diagram" class="mermaid">
Expand Down
3 changes: 3 additions & 0 deletions packages/mermaid/package.json
Expand Up @@ -78,9 +78,12 @@
"dayjs": "^1.11.10",
"dompurify": "^3.0.11",
"elkjs": "^0.9.2",
"highlight.js": "^11.9.0",
"katex": "^0.16.9",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^12.0.1",
"marked-highlight": "^2.1.1",
"mdast-util-from-markdown": "^2.0.0",
"stylis": "^4.3.1",
"ts-dedent": "^2.2.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/mermaid/src/diagrams/common/common.ts
Expand Up @@ -294,6 +294,7 @@ const processSet = (input: string): string => {
export const isMathMLSupported = () => window.MathMLElement !== undefined;

export const katexRegex = /\$\$(.*)\$\$/g;
export const markdownRegex = /```((.|\n)*)```/g;

/**
* Whether or not a text has KaTeX delimiters
Expand All @@ -303,6 +304,14 @@ export const katexRegex = /\$\$(.*)\$\$/g;
*/
export const hasKatex = (text: string): boolean => (text.match(katexRegex)?.length ?? 0) > 0;

/**
* Whether or not a text has markdown delimiters
*
* @param text - The text to test
* @returns Whether or not the text has markdown delimiters
*/
export const hasMarkdown = (text: string): boolean => (text.match(markdownRegex)?.length ?? 0) > 0;

/**
* Computes the minimum dimensions needed to display a div containing MathML
*
Expand Down
Expand Up @@ -83,6 +83,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
\-[\)] return 'SOLID_POINT';
\-\-[\)] return 'DOTTED_POINT';
":"(?:(?:no)?wrap:)?[^#\n;]+ return 'TXT';
""(?:(?:no)?wrap:)?[^#\n;]+ return 'TXT2';
"+" return '+';
"-" return '-';
<<EOF>> return 'NEWLINE';
Expand All @@ -107,6 +108,17 @@ document
| document line {$1.push($2);$$ = $1}
;

note_section
: /* empty */ { $$ = "" }
| note_section note_line {$1=$1.concat($2);$$ = $1}
;

note_line
: ACTOR { $$ = $1 }
| TXT { $$ = $1 }
| NEWLINE { }
;

line
: SPACE statement { $$ = $2 }
| statement { $$ = $1 }
Expand Down Expand Up @@ -241,6 +253,9 @@ note_statement
$2[0] = $2[0].actor;
$2[1] = $2[1].actor;
$$ = [$3, {type:'addNote', placement:yy.PLACEMENT.OVER, actor:$2.slice(0, 2), text:$4}];}
| 'note' placement actor note_section end
{
$$ = [$3, {type:'addNote', placement:$2, actor:$3.actor, text:yy.parseNoteStatement($4)}];}
;

links_statement
Expand Down
23 changes: 22 additions & 1 deletion packages/mermaid/src/diagrams/sequence/sequenceDb.js
@@ -1,5 +1,6 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js';
import { ImperativeState } from '../../utils/imperativeState.js';
import { sanitizeText } from '../common/common.js';
import {
clear as commonClear,
Expand All @@ -10,7 +11,6 @@ import {
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import { ImperativeState } from '../../utils/imperativeState.js';

const state = new ImperativeState(() => ({
prevActor: undefined,
Expand Down Expand Up @@ -268,6 +268,25 @@ export const parseBoxData = function (str) {
};
};

export const parseNoteStatement = function (str) {
try {
const _str = str.trim();
const _text = _str.match(/^:?json:/) !== null
? JSON.stringify(JSON.parse(_str.replace(/^:json:/, '').trim()),null,2)
: _str;
const message = {
text:
_text,
wrap:
false
};
return message;
} catch (exception) {
let error = new Error('Invalid JSON');
throw error;
}
}

export const LINETYPE = {
SOLID: 0,
DOTTED: 1,
Expand Down Expand Up @@ -639,6 +658,7 @@ export default {
clear,
parseMessage,
parseBoxData,
parseNoteStatement,
LINETYPE,
ARROWTYPE,
PLACEMENT,
Expand All @@ -649,4 +669,5 @@ export default {
getAccDescription,
hasAtLeastOneBox,
hasAtLeastOneBoxWithTitle,

};
12 changes: 8 additions & 4 deletions packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
@@ -1,8 +1,8 @@
// @ts-nocheck TODO: fix file
import { select } from 'd3';
import svgDraw, { drawKatex, ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js';
import svgDraw, { drawKatex, ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights, drawMarkdown } from './svgDraw.js';
import { log } from '../../logger.js';
import common, { calculateMathMLDimensions, hasKatex } from '../common/common.js';
import common, { calculateMathMLDimensions, hasKatex, hasMarkdown } from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import assignWithDepth from '../../assignWithDepth.js';
Expand Down Expand Up @@ -263,7 +263,7 @@ const drawNote = async function (elem: any, noteModel: NoteModel) {
textObj.textMargin = conf.noteMargin;
textObj.valign = 'center';

const textElem = hasKatex(textObj.text) ? await drawKatex(g, textObj) : drawText(g, textObj);
const textElem = hasKatex(textObj.text) ? await drawKatex(g, textObj) : hasMarkdown(textObj.text)?await drawMarkdown(g,textObj):drawText(g, textObj);

const textHeight = Math.round(
textElem
Expand Down Expand Up @@ -1340,7 +1340,11 @@ const buildNoteModel = async function (msg, actors, diagObj) {

let textDimensions: { width: number; height: number; lineHeight?: number } = hasKatex(msg.message)
? await calculateMathMLDimensions(msg.message, getConfig())
: utils.calculateTextDimensions(
: hasMarkdown(msg.message)
? await utils.calculateMarkdownDimensions(
msg.message,
noteFont(conf))
: utils.calculateTextDimensions(
shouldWrap ? utils.wrapLabel(msg.message, conf.width, noteFont(conf)) : msg.message,
noteFont(conf)
);
Expand Down
42 changes: 41 additions & 1 deletion packages/mermaid/src/diagrams/sequence/svgDraw.js
@@ -1,6 +1,6 @@
import common, { calculateMathMLDimensions, hasKatex, renderKatex } from '../common/common.js';
import * as svgDrawCommon from '../common/svgDrawCommon.js';
import { ZERO_WIDTH_SPACE, parseFontSize } from '../../utils.js';
import { ZERO_WIDTH_SPACE, parseFontSize, renderMarkdown } from '../../utils.js';
import { sanitizeUrl } from '@braintree/sanitize-url';
import * as configApi from '../../config.js';

Expand Down Expand Up @@ -258,6 +258,46 @@ export const drawText = function (elem, textData) {
return textElems;
};

export const drawMarkdown = async function (elem, textData, msgModel = null) {
let textElem = elem.append('foreignObject');
const lines = await renderMarkdown(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 drawLabel = function (elem, txtObject) {
/**
* @param {any} x
Expand Down
75 changes: 66 additions & 9 deletions packages/mermaid/src/utils.ts
Expand Up @@ -7,12 +7,12 @@ import {
curveBumpX,
curveBumpY,
curveBundle,
curveCardinal,
curveCardinalClosed,
curveCardinalOpen,
curveCardinal,
curveCatmullRom,
curveCatmullRomClosed,
curveCatmullRomOpen,
curveCatmullRom,
curveLinear,
curveLinearClosed,
curveMonotoneX,
Expand All @@ -23,18 +23,20 @@ import {
curveStepBefore,
select,
} from 'd3';
import common from './diagrams/common/common.js';
import { sanitizeDirective } from './utils/sanitizeDirective.js';
import { log } from './logger.js';
import { detectType } from './diagram-api/detectType.js';
import assignWithDepth from './assignWithDepth.js';
import type { MermaidConfig } from './config.type.js';
import hljs from 'highlight.js';
import memoize from 'lodash-es/memoize.js';
import merge from 'lodash-es/merge.js';
import { Marked } from "marked";
import { markedHighlight } from "marked-highlight";
import assignWithDepth from './assignWithDepth.js';
import type { MermaidConfig } from './config.type.js';
import { detectType } from './diagram-api/detectType.js';
import { directiveRegex } from './diagram-api/regexes.js';
import common, { hasMarkdown } from './diagrams/common/common.js';
import { log } from './logger.js';
import type { D3Element } from './mermaidAPI.js';
import type { Point, TextDimensionConfig, TextDimensions } from './types.js';

import { sanitizeDirective } from './utils/sanitizeDirective.js';
export const ZERO_WIDTH_SPACE = '\u200b';

// Effectively an enum of the supported curve types, accessible by name
Expand Down Expand Up @@ -754,6 +756,60 @@ export const calculateTextDimensions: (
(text, config) => `${text}${config.fontSize}${config.fontWeight}${config.fontFamily}`
);

/**
* This calculates the dimensions of the given text, font size, font family, font weight, and
* margins.
*
* @param text - The text to calculate the width of
* @param config - The config for fontSize, fontFamily, fontWeight, and margin all impacting
* the resulting size
* @returns The dimensions for the given text
*/
export const calculateMarkdownDimensions = async (text: string, config: TextDimensionConfig) => {
const { fontSize = 12, fontFamily = 'Arial', fontWeight = 400 } = config;
const [, _fontSizePx="12px"] = parseFontSize(fontSize);
text = await renderMarkdown(text, config);
const divElem = document.createElement('div');
divElem.innerHTML = text;
divElem.id = 'markdown-temp';
divElem.style.visibility = 'hidden';
divElem.style.position = 'absolute';
divElem.style.fontSize = _fontSizePx;
divElem.style.fontFamily = fontFamily;
divElem.style.fontWeight = ""+fontWeight;
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 renderMarkdown = async (text: string, config: MermaidConfig): Promise<string> => {
if (!hasMarkdown(text)) {
return text;
}

const marked = new Marked(
markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang, info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
})
);

return marked.parse(text);
}

export class InitIDGenerator {
private count = 0;
public next: () => number;
Expand Down Expand Up @@ -870,6 +926,7 @@ export default {
calculateTextHeight,
calculateTextWidth,
calculateTextDimensions,
calculateMarkdownDimensions,
cleanAndMerge,
detectInit,
detectDirective,
Expand Down
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 379da2b

Please sign in to comment.