-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #150 from cm-wada-yusuke/feature/mermaid
feat: mermaid.js 対応
- Loading branch information
Showing
8 changed files
with
411 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 --> 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 --> 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 --> B\n<br><script>alert("XSS")</script>```</pre></embed-mermaid></div>' | ||
); | ||
}); | ||
}); |
Oops, something went wrong.