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'}