Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add markdown support to sequence diagram notes #5705

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cypress/integration/rendering/sequencediagram.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,33 @@ describe('Sequence diagram', () => {
`
);
});
it('should render a sequence diagram with markdown notes', () => {
imgSnapshotTest(
`
sequenceDiagram
participant a as Alice
participant j as John
a->>j: Hello John, how are you?
note over a, j: [ This is a **bold** note
\`\`\`json
{
"name": "Alice",
"age": 30
}
\`\`\`
]
j-->>a: Great!
note right of a:
[
This is another \`markdown\` note
]
`,
{
logLevel: 0,
sequence: { mirrorActors: false, noteFontSize: 18, noteFontFamily: 'Arial' },
}
);
});
describe('font settings', () => {
it('should render different note fonts when configured', () => {
imgSnapshotTest(
Expand Down
51 changes: 51 additions & 0 deletions demos/sequence.html
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,57 @@ <h1>Sequence diagram demos</h1>
Alice<<->>Bob: Wow, we said that at the same time!
Bob<<-->>Alice: Bidirectional Arrows are so cool
</pre>

<hr />

<pre class="mermaid">
sequenceDiagram
actor Alice
actor John
Alice-xJohn: Hello John, how are you?
note over Alice,John: [ *This* is some `markdown` text
```json
{
"title": "This is a note",
"description": "This is a note with a description"
}
```
]
John--xAlice: Great!
note left of Alice: [ This is some `markdown` text
```json
{
"title": "This is a note",
from: "Alice",
to: "John",
"subtitle": "A very pretty one note",
"description": "This is a note with a description"
}
```
]
John--xMaria: And you?
note over John,Maria: [ This is some `markdown` text
```json
{
"title": "This is a note",
"subtitle": "A very pretty one",
"description": "This is a note with a description",
"things": ["one", "two", "three"]
}
```
There can _even_ be `more markdown`:
```
const note = "pretty";
if (note === "pretty") {
console.log("This is a note with a description");
} else {
// do something else
}
```
]
Maria--xBob: I'm fine, thanks!
</pre>

<script type="module">
import mermaid from './mermaid.esm.mjs';
mermaid.initialize({
Expand Down
8 changes: 4 additions & 4 deletions docs/config/setup/interfaces/mermaid.DetailedError.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

#### Defined in

[packages/mermaid/src/utils.ts:785](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L785)
[packages/mermaid/src/utils.ts:787](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L787)

---

Expand All @@ -26,7 +26,7 @@

#### Defined in

[packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L783)
[packages/mermaid/src/utils.ts:785](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L785)

---

Expand All @@ -36,7 +36,7 @@

#### Defined in

[packages/mermaid/src/utils.ts:786](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L786)
[packages/mermaid/src/utils.ts:788](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L788)

---

Expand All @@ -46,4 +46,4 @@

#### Defined in

[packages/mermaid/src/utils.ts:781](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L781)
[packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L783)
2 changes: 1 addition & 1 deletion docs/config/setup/interfaces/mermaid.Mermaid.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Internal helpers for mermaid
| `common.sanitizeTextOrArray` | (`a`: `string` \| `string`\[] \| `string`\[]\[], `config`: [`MermaidConfig`](mermaid.MermaidConfig.md)) => `string` \| `string`\[] |
| `common.splitBreaks` | (`text`: `string`) => `string`\[] |
| `getConfig` | () => [`MermaidConfig`](mermaid.MermaidConfig.md) |
| `insertCluster` | (`elem`: `any`, `node`: `any`) => `any` |
| `insertCluster` | (`elem`: `any`, `node`: `any`) => `Promise`<`any`> |
| `insertEdge` | (`elem`: `any`, `edge`: `any`, `clusterDb`: `any`, `diagramType`: `any`, `startNode`: `any`, `endNode`: `any`, `id`: `any`) => { `originalPath`: `any` ; `updatedPath`: `any` } |
| `insertEdgeLabel` | (`elem`: `any`, `edge`: `any`) => `Promise`<`any`> |
| `insertMarkers` | (`elem`: `any`, `markerArray`: `any`, `type`: `any`, `id`: `any`) => `void` |
Expand Down
39 changes: 39 additions & 0 deletions docs/syntax/sequenceDiagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,45 @@ sequenceDiagram
Note over Alice,John: A typical interaction
```

## Markdown Notes (v10.8.0+)

It is possible to use markdown in notes on sequence diagrams. This is done by the notation
Note \[ right of | left of | over ] \[Actor]: \[ Markdown Text ]

See the example below:

````mermaid-example
sequenceDiagram
participant John
Note right of John: [
This is some text which *will* be handled by _marked_ to support `markdown syntax`
```json
{
"status": 200,
"data": [
{ name: "John", age: 28 }
]
}
```
]
````

````mermaid
sequenceDiagram
participant John
Note right of John: [
This is some text which *will* be handled by _marked_ to support `markdown syntax`
```json
{
"status": 200,
"data": [
{ name: "John", age: 28 }
]
}
```
]
````

## Line breaks

Line break can be added to Note and Message:
Expand Down
4 changes: 3 additions & 1 deletion packages/mermaid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@
"dagre-d3-es": "7.0.10",
"dayjs": "^1.11.10",
"dompurify": "^3.0.11",
"highlight.js": "^11.10.0",
"katex": "^0.16.9",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^13.0.2",
"marked": "^14.0.0",
"marked-highlight": "^2.1.4",
"roughjs": "^4.6.6",
"stylis": "^4.3.1",
"ts-dedent": "^2.2.0",
Expand Down
102 changes: 99 additions & 3 deletions packages/mermaid/src/diagrams/common/common.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import DOMPurify from 'dompurify';
import hljs from 'highlight.js';
import type { Token, Tokens } from 'marked';
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import type { MermaidConfig } from '../../config.type.js';

// Remove and ignore br:s
export const lineBreakRegex = /<br\s*\/?>/gi;
import { parseFontSize, lineBreakRegex } from '../../utils.js';
import type { TextDimensionConfig } from '../../types.js';

/**
* Gets the rows of lines in a string
Expand Down Expand Up @@ -367,6 +370,99 @@ export const renderKatex = async (text: string, config: MermaidConfig): Promise<
);
};

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

/**
* 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;

function isCodeToken(animal: Token): animal is Tokens.Code {
return (animal as Tokens.Code).lang !== undefined;
}

/**
* Attempts to render and return the markdown portion of a string with Marked
*
* @param text - The text to test
* @param config - Configuration for Mermaid
* @returns String containing markdown rendered as html, or an error message if it is not and stylesheets aren't present
*/
export const renderMarkdown = async (text: string): Promise<string> => {
if (!hasMarkdown(text)) {
return text;
}

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

// make sure every code block has a language - design will look bad otherwise
marked.use({
hooks: {
processAllTokens: (tokens) =>
tokens.map((token) => {
if (isCodeToken(token)) {
return {
...token,
lang: hljs.highlightAuto(token.text, [
'json',
'javascript',
'typescript',
'html',
'xml',
'java',
]).language,
};
}
return token;
}),
},
});

return marked.parse(text);
};

/**
* 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);
const divElem = document.createElement('div');
divElem.innerHTML = text;
divElem.id = 'markdown-temp';
divElem.style.visibility = 'hidden';
divElem.style.position = 'absolute';
divElem.style.padding = '1em';
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;
};

export default {
getRows,
sanitizeText,
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/src/diagrams/common/svgDrawCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
TextData,
TextObject,
} from './commonTypes.js';
import { lineBreakRegex } from './common.js';
import { lineBreakRegex } from '../../utils.js';

export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElement => {
const rectElement: D3RectElement = element.append('rect');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
%x acc_title
%x acc_descr
%x acc_descr_multiline
%x text
%%

<text>"]" { this.counter--; if (this.counter === 0) {this.popState(); return 'MDE'; } return 'SQE'; }
<text>"[" { this.counter++; return 'SQS' }
<text>[^\]\[]+ { return 'MDC'; }
<INITIAL>(":"\s*[\n\s]*\[) { this.pushState("text"); this.counter = 1; return 'MDS'; }
[\n]+ return 'NEWLINE';
\s+ /* skip all whitespace */
<ID,ALIAS,LINE>((?!\n)\s)+ /* skip same-line whitespace */
Expand Down Expand Up @@ -84,12 +89,13 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
\-\-[x] return 'DOTTED_CROSS';
\-[\)] return 'SOLID_POINT';
\-\-[\)] return 'DOTTED_POINT';
":"(?:(?:no)?wrap:)?[^#\n;]+ return 'TXT';
":"(?![\n\s]*\[)(?:(?:no)?wrap:)?[^#\n;]+ return 'TXT';
"+" return '+';
"-" return '-';
<<EOF>> return 'NEWLINE';
. return 'INVALID';


/lex

%left '^'
Expand Down Expand Up @@ -142,7 +148,7 @@ statement
| autonumber 'NEWLINE' {$$ = {type:'sequenceIndex', sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER}; }
| 'activate' actor 'NEWLINE' {$$={type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $2.actor};}
| 'deactivate' actor 'NEWLINE' {$$={type: 'activeEnd', signalType: yy.LINETYPE.ACTIVE_END, actor: $2.actor};}
| note_statement 'NEWLINE'
| note_statement
| links_statement 'NEWLINE'
| link_statement 'NEWLINE'
| properties_statement 'NEWLINE'
Expand Down Expand Up @@ -243,8 +249,33 @@ 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 MDS mdtext MDE 'NEWLINE'
{
$$ = [$3, {type:'addNote', placement:$placement, actor:$actor.actor, text: $mdtext }];}
| 'note' 'over' actor_pair MDS mdtext MDE 'NEWLINE'
{
// Coerce actor_pair into a [to, from, ...] array
$2 = [].concat($3, $3).slice(0, 2);
$2[0] = $2[0].actor;
$2[1] = $2[1].actor;
$$ = [$3, {type:'addNote', placement:yy.PLACEMENT.OVER, actor:$2.slice(0, 2), text: $mdtext}];}
;

mdtext
: mdtext SQS mdtext SQE
{
$$ = {type: 'text', text: `${$1.text}[${$3.text}]`};
}
| mdtext MDC
{
$$ = {type: 'text', text: `${$1.text}${$2}`};
}
| MDC
{
$$ = {type: 'text', text: $1};
}
;

links_statement
: 'links' actor text2
{
Expand Down
Loading
Loading