マークダウンテキストを HTML 表示するライブラリ試してみたのでメモ
結論
React で使うなら React-markdown がシンプルで良さそう
調査経緯
- ライブラリとしては marked >>> markdown-it > remark の順で使われている
- remark の内部で使われている remark-parse が圧倒的に多く使われている
- 最終更新は markdown-it > remark (remark-parse) > marked の順
marked は利用数は多いのですが最終アップデートが 11 年前なので、Markdown-it と Remark で検討することにしました
Markdown-it | Remark | |
---|---|---|
Project | JavaScript | TypeScript |
Star | 14.5k | 5.8k |
Last publish | 7 months ago | a year ago |
Star 数は Markdown-it の方が多いですが、Remark の方は unified というプロジェクトに属していて TypeScript 製です。
GitHub のリポジトリをみた感じ両方とも活発に開発されているように感じました。Remark の方は Next.js の Vercel がスポンサーをしているらしく個人的にはポイントが高いです。
Markdown-it
ライブラリのインストール
$ npm i markdown-it
$ npm i -D @types/markdown-it
使い方
import markdownit from 'markdown-it'; const markdownString = `# headind`; const Content = () => { return ( <div dangerouslySetInnerHTML={{ __html: markdownit().render(markdownString) }} /> ); };
- markdown-it は HTML な文字列に変換するので React で出力するには
dangerouslySetInnerHTML
を使う必要がある - プラグインなど導入しなくても table も変換される
- GitHub にあるような checkbox はサポートされてない
<script>
含む HTML はそのまま文字列として出力される
Remark
If you just want to turn markdown into HTML (with maybe a few extensions), we recommend
micromark
instead. remark can also do that but it focusses on ASTs and providing an interface for plugins to transform them.Depending on the input you have and output you want, you can use different parts of
remark
. If the input is markdown, you can useremark-parse
with unified. If the output is markdown, you can useremark-stringify
withunified
If both the input and output are markdown, you can use remark on its own. When you want to inspect and format markdown files in a project, you can useremark-cli
.
cf. When should I use this?
今回はあえて Remark の含まれる unified を使った方法を試しました。
ライブラリのインストール
$ npm i unified remark-parse remark-rehype rehype-sanitize rehype-stringify
# Support GFM (tables, autolinks, tasklists, strikethrough)
$ npm i remark-gfm
使い方
Remark は非同期で変換を行うので使い方に注意が必要
import { useEffect, useState } from 'react'; import { unified } from 'unified'; // markdown をパースする import remarkParse from 'remark-parse'; // Support GFM (tables, autolinks, tasklists, strikethrough) import remarkGfm from 'remark-gfm'; // HTML に変換する import remarkRehype from 'remark-rehype'; // サニタイズ import rehypeSanitize from 'rehype-sanitize'; // HTML にシリアライズ import rehypeStringify from 'rehype-stringify'; const markdownString = `# headind`; const parseMarkdown = async (text: string): Promise<string> => { const file = await unified() .use(remarkParse) .use(remarkGfm) .use(remarkRehype) .use(rehypeSanitize) .use(rehypeStringify) .process(text); // VText が返されるので String にして返す return String(file); }; const Content = () => { const [content, setContent] = useState(''); useEffect(() => { const getContent = async () => { const htmlString = await parseMarkdown(markdownString); setContent(htmlString); }; getContent(); }, []); return ( <> {content ? ( <div dangerouslySetInnerHTML={{ __html: content }} /> ) : ( <p>Loading...</p> )} </> ); };
- Remark での変換も HTML な文字列に変換するので React で出力するには
dangerouslySetInnerHTML
を使う必要がある remark-gfm
プラグインを使えば Table, checkbox も変換される<script>
含む HTML はデフォルトでは除去される。オプションで出力を許可することもできる
react-markdown
Remark ベースで React Component としてマークダウンを扱えるようになるライブラリ。Remark ベースなのでプラグインがそのまま利用できる
パッケージのインストール
$ npm i react-markdown
# Support GFM (tables, autolinks, tasklists, strikethrough)
$ npm i remark-gfm
使い方
import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; const markdownString = `# headind`; const Content = () => { return ( <ReactMarkdown remarkPlugins={[remarkGfm]}> {markdownString} </ReactMarkdown> ); };
<ReactMarkdown>
の children に マークダウン形式の文字列を渡すだけでOKremark-gfm
プラグインを使えば Table, checkbox も変換される<script>
含む HTML はそのまま文字列として出力される
HTML タグを使えるようにする
rehype-raw
プラグインを使えばマークダウンの中に直接書かれた HTML も出力できるようになるrehype-sanitize
を合わせて使うことで<script>
タグを除去できる
プラグインのインストール
$ npm i rehype-raw rehype-sanitize
使い方
import ReactMarkdown from 'react-markdown'; import rehypeRaw from "rehype-raw"; import rehypeSanitize from "rehype-sanitize"; import remarkGfm from "remark-gfm"; const markdownString = `# headind <strong>Strong</strong> <script>console.log('danger!')</script> `; const Content = () => { return ( <ReactMarkdown rehypePlugins={[rehypeRaw, rehypeSanitize]} remarkPlugins={[remarkGfm]} > {markdownString} </ReactMarkdown> ); };
⚠ プラグインの順番を [rehypeSanitize, rehypeRaw]
にすると <script>
以外の HTML タグも除去されてしまう
結論
Markdown と一言で言ってもフォーマットや形式がふわっとしているので、自分の頭の中にある Markdown がすべてサポートされているのようなことはない。
Remark はプラグインを使うことで使えるフォーマットをカスタマイズできる仕様になっているのが個人的に良いなと感じた。その上で React で使うなら内部的に Remark が使われている React-markdown を使うのがシンプルに書けて良さそうに思った。
これらのライブラリを使えば外部の markdown を fetch して表示する blog のようなものは割と簡単に作れそうだな〜と思いました。ライブラリありがたい!
調べてたら結局長くなってしまった…
おわり
[参考]