かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React マークダウンを HTML 変換するライブラリ試してみた

マークダウンテキストを HTML 表示するライブラリ試してみたのでメモ

結論

React で使うなら React-markdown がシンプルで良さそう

Markdown ライブラリ比較

調査経緯

markdown-it vs remark vs marked

  • ライブラリとしては 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 use remark-parse with unified. If the output is markdown, you can use remark-stringify with unified 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 use remark-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>
      )}
    </>
  );
};

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 に マークダウン形式の文字列を渡すだけでOK
  • remark-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 のようなものは割と簡単に作れそうだな〜と思いました。ライブラリありがたい!

調べてたら結局長くなってしまった…
おわり


[参考]