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

feat: mermaid.js 対応 #150

Merged
merged 15 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from 13 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
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"
```
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note]

  • サンプルを書く先のファイル(embed.md or example.md)
  • どのくらい例を書くか

アドバイスいただければと:bow:

160 changes: 160 additions & 0 deletions packages/zenn-embed-elements/src/classes/mermaid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* 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) {
return false;
Copy link
Contributor

@catnose99 catnose99 Jun 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「レンダリングがなぜか上手くいかない」というケースでの原因究明のため、eを一応consoleに出力するようにしますか?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちら承知しました。

}
})();
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>`,
},
};
Copy link
Member Author

@cm-wada-yusuke cm-wada-yusuke Jun 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] エラー時の表示方法を相談したいです。変わりのHTMLを表示するのは GitLab のマネです。

}

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