Skip to content

Commit

Permalink
Merge pull request #150 from cm-wada-yusuke/feature/mermaid
Browse files Browse the repository at this point in the history
feat: mermaid.js 対応
  • Loading branch information
catnose99 authored Jun 7, 2021
2 parents 2ccb384 + a4d0701 commit 3ef1ecd
Show file tree
Hide file tree
Showing 8 changed files with 411 additions and 3 deletions.
191 changes: 190 additions & 1 deletion packages/example/articles/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,193 @@ $a^*b$ with $a^*$
$\sum_{i=1}^n$
fafa

a
a



## mermaid.js

```mermaid
%%{init: { 'theme': 'forest' } }%%
graph LR;
A-->B & C-->D & E-->F & Z-->X;
F-->G
G-->H
H-->I
I-->J
J-->K
K-->L
L-->M
M-->N
N-->O
O-->P
P-->ID1[ノード1<br>ノード2]
```

### flowchart

```mermaid
flowchart TB
c1-->a2
subgraph one
a1-->a2
end
subgraph two
b1-->b2
end
subgraph three
c1-->c2
end
one --> two
three --> two
two --> c2
```

### sequence diagram

```mermaid
sequenceDiagram
autonumber
アリス->>光輝: Hello John, how are you?
loop Healthcheck
光輝->>光輝: Fight against hypochondria
end
Note right of 光輝: Rational thoughts!
光輝-->>アリス: Great!
光輝->>Bob: How about you?
Bob-->>光輝: Jolly good!
```

### class diagram

```mermaid
classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}
```


### state diagram

```mermaid
stateDiagram-v2
[*] --> Active
state Active {
[*] --> NumLockOff
NumLockOff --> NumLockOn : EvNumLockPressed
NumLockOn --> NumLockOff : EvNumLockPressed
--
[*] --> CapsLockOff
CapsLockOff --> CapsLockOn : EvCapsLockPressed
CapsLockOn --> CapsLockOff : EvCapsLockPressed
--
[*] --> ScrollLockOff
ScrollLockOff --> ScrollLockOn : EvScrollLockPressed
ScrollLockOn --> ScrollLockOff : EvScrollLockPressed
}
```



```mermaid
graph LR
A:::someclass B
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
classDef someclass fill:#f96;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
A-->B & C-->D & E-->F & Z-->X;
```


```mermaid
graph LR;
A["<img src=invalid onerror=alert('XSS')></img>"] --> B;
alert`md5_salt`-->B;
click alert`md5_salt` eval "Tooltip for a callback"
click B "javascript:alert('XSS')" "This is a tooltip for a link"
```

```mermaid
graph LR;
alert`md5_salt`-->B;
click alert`md5_salt` eval "Tooltip for a callback"
click B "javascript:alert('XSS')" "This is a tooltip for a link"
link Zebra "http://www.github.com" "This is a link"
```
164 changes: 164 additions & 0 deletions packages/zenn-embed-elements/src/classes/mermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* original: https://github.com/gitlabhq/gitlabhq/blob/master/app/assets/javascripts/behaviors/markdown/render_mermaid.js
*/

import { loadScript } from '../utils/load-script';

// レンダリングする図ごとの最大文字数
const MAX_CHAR_LIMIT = 2000;

// https://mermaid-js.github.io/mermaid/#/flowchart?id=chaining-of-links
// 新しい仕様で
// graph LR
// a --> b & c--> d
// に対応するが、少ない記述でノード接続が爆発する可能性があるため最大数を制限する
const MAX_CHAINING_OF_LINKS_LIMIT = 10;

// Page values
declare let mermaid: any;
const containerId = 'mermaid-container';

async function initMermaid(): Promise<void> {
if (typeof mermaid === 'undefined') {
await loadScript({
src: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/mermaid.min.js',
id: 'mermaid-js',
});
const theme = 'default';

// mermaid 本体がロード時に走らないように設定
// mermaid 本体は使わないのでほかは設定しない
mermaid!.initialize({
mermaid: {
startOnLoad: false,
},
});

// mermaidAPI の設定
mermaid!.mermaidAPI.initialize({
startOnLoad: false, // レンダリングはこちらでやるので false
securityLevel: 'strict', // tags in text are encoded, click functionality is disabled
theme,
er: {
useMaxWidth: true,
},
flowchart: {
useMaxWidth: true, // 表示の都合上見切れるのもスクロールするのも嫌なので最大幅を有効にする
htmlLabels: false, // セキュリティのため、HTMLラベルは許可しない
},
sequence: {
useMaxWidth: true,
},
});
}
}

type ErrorContainer = {
yes: boolean;
message: string;
};

type PotentialRisk = {
syntaxError: ErrorContainer;
charLimitOver: ErrorContainer;
chainingOfLinksOver: ErrorContainer;
};

function getPotentialPerformanceRisk(source: string): PotentialRisk {
const cool = (() => {
try {
mermaid!.mermaidAPI.parse(source);
return true;
} catch (e) {
console.log(
'mermaid.js のレンダリングでシンタックスエラーが発生しました',
e
);
return false;
}
})();
return {
syntaxError: {
yes: !cool,
message: `<li>シンタックスエラーです</li>`,
},
charLimitOver: {
yes: source.length > MAX_CHAR_LIMIT,
message: `<li>ブロックあたりの文字数上限は${MAX_CHAR_LIMIT}です</li>`,
},
chainingOfLinksOver: {
yes: (source.match(/&/g) || []).length > MAX_CHAINING_OF_LINKS_LIMIT,
message: `<li>ブロックあたりの&によるチェイン上限は${MAX_CHAINING_OF_LINKS_LIMIT}です</li>`,
},
};
}

export class EmbedMermaid extends HTMLElement {
// mermaid のソース記述が格納されているpreタグ
private readonly _sourceContainer: HTMLPreElement;

// 描画後の svg を格納するdivタグ ここで作る
private readonly _svgContainer: HTMLDivElement;

constructor() {
super();

// コード記述が格納されている pre タグを取得
this._sourceContainer = this.childNodes[0] as HTMLPreElement;

// 描画後のSVGを格納する div タグを作成
const container = document.createElement('div');
this.appendChild(container);
this._svgContainer = container;
}

async connectedCallback() {
this.render();
}

async render() {
await initMermaid();
const source = this._sourceContainer.innerText || '';

// Mermaid モジュールの読み込みに失敗したり、レンダリング対象のコンテンツが空の場合は何もせずに終了
if (!source) {
return;
}

// 文法エラーやパフォーマンスリスクが検出された場合、注意書きをレンダリングして終了
const risk = getPotentialPerformanceRisk(source);
if (
Object.values(risk)
.map((r) => r.yes)
.includes(true)
) {
this.innerHTML = `
<p>
<span>mermaidをレンダリングできません。</span>
<ul>
${risk.syntaxError.yes ? risk.syntaxError.message : ''}
${risk.charLimitOver.yes ? risk.charLimitOver.message : ''}
${risk.chainingOfLinksOver.yes ? risk.chainingOfLinksOver.message : ''}
</ul>
</p>
`;
return;
}

// すべて通過した場合はレンダリングする
// セキュリティリスクを考慮して bindFunctions は実行しない方針にする
// 今回は `securityLevel='strict'` にしているのでどのみち実行されない
// securityLevel='loose'にし、かつ `Interaction` を有効にする場合は
// https://github.com/mermaidjs/mermaid-gitbook/blob/master/content/usage.md#binding-events
// ここを参考に追加する
const insert = (svgCode: string, bindFunctions: any) => {
this._svgContainer.innerHTML = svgCode;
};
mermaid?.mermaidAPI.render(
`${containerId}-${Date.now().valueOf()}-render`,
source,
insert,
this._sourceContainer
);
}
}
2 changes: 2 additions & 0 deletions packages/zenn-embed-elements/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { EmbedGist } from './classes/gist';
import { EmbedTweet } from './classes/tweet';
import { EmbedKatex } from './classes/katex';
import { EmbedMermaid } from './classes/mermaid';

customElements.define('embed-gist', EmbedGist);
customElements.define('embed-tweet', EmbedTweet);
customElements.define('embed-katex', EmbedKatex);
customElements.define('embed-mermaid', EmbedMermaid);
24 changes: 24 additions & 0 deletions packages/zenn-markdown-html/__tests__/mermaid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import markdownToHtml from '../src/index';

describe('Detect mermaid property', () => {
test('should generate TD valid code format html', () => {
const html = markdownToHtml(`\`\`\`mermaid\ngraph TD\nA --> B\n\`\`\``);
expect(html).toContain(
'<div class="embed-mermaid"><embed-mermaid><pre class="zenn-mermaid">graph TD\nA --&gt; B</pre></embed-mermaid></div>'
);
});
test('should keep directive', () => {
const html = markdownToHtml(`\`\`\`mermaid\n%%{init: { 'theme': 'forest' } }%%\ngraph TD\nA --> B\n\`\`\``);
expect(html).toContain(
'<div class="embed-mermaid"><embed-mermaid><pre class="zenn-mermaid">%%{init: { \'theme\': \'forest\' } }%%\ngraph TD\nA --&gt; B</pre></embed-mermaid></div>'
);
});
test('should escape html tag', () => {
const html = markdownToHtml(
`\`\`\`mermaid\ngraph TD\nA --> B\n<br><script>alert("XSS")</script>\`\`\``
);
expect(html).toContain(
'<div class="embed-mermaid"><embed-mermaid><pre class="zenn-mermaid">graph TD\nA --&gt; B\n&lt;br&gt;&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;```</pre></embed-mermaid></div>'
);
});
});
Loading

0 comments on commit 3ef1ecd

Please sign in to comment.