From 379da2ba599840067d3ef0fb90dbf0b160d332e5 Mon Sep 17 00:00:00 2001 From: DarkEdges Date: Sun, 14 Apr 2024 07:12:11 +1000 Subject: [PATCH] 5460 - Initial Release for review https://github.com/mermaid-js/mermaid/issues/5460 This adds Markdown support for SequenceDiagram notes Needs cleanup. --- demos/dev/example.html | 1 + packages/mermaid/package.json | 3 + .../mermaid/src/diagrams/common/common.ts | 9 +++ .../sequence/parser/sequenceDiagram.jison | 15 ++++ .../src/diagrams/sequence/sequenceDb.js | 23 +++++- .../src/diagrams/sequence/sequenceRenderer.ts | 12 ++- .../mermaid/src/diagrams/sequence/svgDraw.js | 42 ++++++++++- packages/mermaid/src/utils.ts | 75 ++++++++++++++++--- pnpm-lock.yaml | 28 +++++++ 9 files changed, 193 insertions(+), 15 deletions(-) diff --git a/demos/dev/example.html b/demos/dev/example.html index 27d31e177a..f49e94dd85 100644 --- a/demos/dev/example.html +++ b/demos/dev/example.html @@ -19,6 +19,7 @@ flex: 1; } +
diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json
index 9a91a645e1..cb7ba09c45 100644
--- a/packages/mermaid/package.json
+++ b/packages/mermaid/package.json
@@ -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",
diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts
index 017b2b0911..5a027d4676 100644
--- a/packages/mermaid/src/diagrams/common/common.ts
+++ b/packages/mermaid/src/diagrams/common/common.ts
@@ -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
@@ -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
  *
diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
index 78b0c9ed9e..684cb05ac4 100644
--- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
+++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison
@@ -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 '-';
 <>                                                         return 'NEWLINE';
@@ -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 }
@@ -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
diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.js b/packages/mermaid/src/diagrams/sequence/sequenceDb.js
index 4ff1982275..80360c264f 100644
--- a/packages/mermaid/src/diagrams/sequence/sequenceDb.js
+++ b/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,
@@ -10,7 +11,6 @@ import {
   setAccTitle,
   setDiagramTitle,
 } from '../common/commonDb.js';
-import { ImperativeState } from '../../utils/imperativeState.js';
 
 const state = new ImperativeState(() => ({
   prevActor: undefined,
@@ -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,
@@ -639,6 +658,7 @@ export default {
   clear,
   parseMessage,
   parseBoxData,
+  parseNoteStatement,
   LINETYPE,
   ARROWTYPE,
   PLACEMENT,
@@ -649,4 +669,5 @@ export default {
   getAccDescription,
   hasAtLeastOneBox,
   hasAtLeastOneBoxWithTitle,
+
 };
diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts
index 98fdcddc40..d52f7e1d31 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, { 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';
@@ -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
@@ -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)
       );
diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js
index 84351ea5ad..82bf4ffc85 100644
--- a/packages/mermaid/src/diagrams/sequence/svgDraw.js
+++ b/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';
 
@@ -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
diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts
index 06aca5ab27..c31de9294c 100644
--- a/packages/mermaid/src/utils.ts
+++ b/packages/mermaid/src/utils.ts
@@ -7,12 +7,12 @@ import {
   curveBumpX,
   curveBumpY,
   curveBundle,
+  curveCardinal,
   curveCardinalClosed,
   curveCardinalOpen,
-  curveCardinal,
+  curveCatmullRom,
   curveCatmullRomClosed,
   curveCatmullRomOpen,
-  curveCatmullRom,
   curveLinear,
   curveLinearClosed,
   curveMonotoneX,
@@ -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
@@ -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 => {
+  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;
@@ -870,6 +926,7 @@ export default {
   calculateTextHeight,
   calculateTextWidth,
   calculateTextDimensions,
+  calculateMarkdownDimensions,
   cleanAndMerge,
   detectInit,
   detectDirective,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ffa09eb3c9..7624f4fcbc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -232,6 +232,9 @@ importers:
       elkjs:
         specifier: ^0.9.2
         version: 0.9.2
+      highlight.js:
+        specifier: ^11.9.0
+        version: 11.9.0
       katex:
         specifier: ^0.16.9
         version: 0.16.10
@@ -241,6 +244,12 @@ importers:
       lodash-es:
         specifier: ^4.17.21
         version: 4.17.21
+      marked:
+        specifier: ^12.0.1
+        version: 12.0.1
+      marked-highlight:
+        specifier: ^2.1.1
+        version: 2.1.1(marked@12.0.1)
       mdast-util-from-markdown:
         specifier: ^2.0.0
         version: 2.0.0
@@ -9993,6 +10002,11 @@ packages:
     resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
     dev: false
 
+  /highlight.js@11.9.0:
+    resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==}
+    engines: {node: '>=12.0.0'}
+    dev: false
+
   /hookable@5.5.3:
     resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
     dev: true
@@ -11869,6 +11883,20 @@ packages:
     resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
     dev: true
 
+  /marked-highlight@2.1.1(marked@12.0.1):
+    resolution: {integrity: sha512-ktdqwtBne8rim5mb+vvZ9FzElGFb+CHCgkx/g6DSzTjaSrVnxsJdSzB5YgCkknFrcOW+viocM1lGyIjC0oa3fg==}
+    peerDependencies:
+      marked: '>=4 <13'
+    dependencies:
+      marked: 12.0.1
+    dev: false
+
+  /marked@12.0.1:
+    resolution: {integrity: sha512-Y1/V2yafOcOdWQCX0XpAKXzDakPOpn6U0YLxTJs3cww6VxOzZV1BTOOYWLvH3gX38cq+iLwljHHTnMtlDfg01Q==}
+    engines: {node: '>= 18'}
+    hasBin: true
+    dev: false
+
   /marked@4.3.0:
     resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
     engines: {node: '>= 12'}