Remark・Rehype を使って Markdown から HTML に変換する

Gatsby.js を使っていて、Markdown ファイルをブログ記事として投稿できる gatsby-transformer-remark というパッケージを使ってみた。

プラグインを入れていくと、Frontmatter と呼ばれる YAML テンプレート部分を解釈できるようで、同様の仕組みは Hexo という静的サイトジェネレータでも導入されている。

こうした仕組みが面白そうだったので、内部実装を調べてみると、Remark とか Rehype とかいうパッケージが利用されていた。

目次

Remark とは

はじめに、今回紹介するのは、以下の gnab/remark ではない

今回紹介するのは、公式サイト remark.js.org、remarkjs/remark だ。

Remark は、Unified.js という規格に沿って作られた Markdown のパーサで、様々な機能をプラグインで導入できる。

Remark に関する代表的なパッケージは以下の2つ。

上の2つだけ見ると、「Markdown → mdast → Markdown」としか扱えず、HTML 化できないんじゃないの?と思われるだろう。そこで登場するのが Rehype だ。

Rehype とは

Rehype は、Unified.js の規格に沿って作られた HTML のパーサだ。コチラにも以下のようなパッケージがある。

コチラもコレだけ見ると、「HTML → hast → HTML」としか扱えない。

Markdown と HTML の相互変換

じゃあ Markdown から HTML に変換するにはどうしたらいいかというと、変換用のパッケージがある。

つまり、一度 AST に変換して、それから双方の AST へと変換するワケだ。

ということで Markdown から HTML へと変換するには、

  1. remark-parse で Markdown を mdast に変換する
  2. remark-rehype で mdast から hast に変換する
  3. rehype-stringify で hast から HTML に変換する

と、3つの手順を踏むことになる。

練習リポジトリ公開

以降、色々と解説していくが、先に Remark を触ってみた練習用リポジトリを紹介しておく。

コード全量はコチラにあるのでご参考までに。

実際にやってみる

それでは実際に変換してみよう。作業用ディレクトリを作り、package.json を作っておく。

$ npm init -y

そしたら以下のパッケージをインストールする。

$ npm install -S unified@9.0.2 remark-parse@8.0.3 remark-rehype@8.0.0 rehype-stringify@8.0.0

パッケージのバージョンを指定しているが、特に重要なのは remark-parse@8.0.3。remark-parse は最近 v9.0.0 にメジャーバージョンアップして、Pedantic という Markdown の仕様が扱えなくなった。

Pedantic Markdown は、CommonMark とは若干仕様が異なり、分かち書きをせずアンダースコア _ による強調ができたりする。しかしその挙動にはバグが多く、remark-parse v9 ではオミットされたのだ。

英語圏の人間にとってはバグに近い挙動が多く不要とされたようだが、分かち書きがない日本語に対してはアスタリスクよりもアンダースコアで強調した方が生の Markdown が見やすく、個人的には気に入っている。そのため、ココではあえて旧バージョンである remark-parse v8.0.3 をインストールしている。

ちなみに、remark-parse v9 からは内部実装が大きく変わっていて、remark-parse は mdast ファミリのユーティリティにほとんどの処理を横流ししているだけ。mdast のユーティリティは内部で micromark というパーサを使っていて、実体 (実処理) は micromark が持っている。remark 本体は CommonMark のパースにしか対応しておらず、GFM (GitHub Flavored Markdown) や Footnotes などの拡張構文については別途プラグインが存在する。Remark はただのインターフェースでしかないので、調節 Micromark を使う方が柔軟なこともありそうだ…。


他のパッケージのバージョンは、自分が検証したバージョン番号。原則、本稿執筆時点の最新版なのでご安心を。

パッケージをインストールしたら、index.js を作って以下のように実装していく。

const fs = require('fs').promises;

const unified = require('unified');
const remarkParse = require('remark-parse');
const remarkRehype = require('remark-rehype');
const rehypeStringify = require('rehype-stringify');

(async () => {
  // Markdown テキストをファイルから取得する
  const inputMarkdownText = await fs.readFile('./input.md', 'utf-8');
  
  // 変換するプラグインを、変換したい順に連ねていく
  const processor = unified()
    .use(remarkParse)       // Markdown → mdast
    .use(remarkRehype)      // mdast → hast
    .use(rehypeStringify);  // hast → HTML
  // パーサを実行する
  const result = await processor.process(inputMarkdownText);
  
  // 生成した HTML をファイルに書き出す
  const outputHtmlText = result.contents;
  await fs.writeFile('./output.html', outputHtmlText, 'utf-8');
})();

こんな感じで、input.md の内容が output.html に変換されて出力される。

今回はココまで

Remark や Unified.js 関連の npm パッケージは細かく分割されていて、やりたいことに対して何をどう組み合わせたらいいのかが物凄く分かりづらい。最近 Remark 周りのメジャーバージョンアップがあったことで、最新版では使えなくなっているパッケージも多かった。そもそも文献が少ないので、メチャクチャググって、トライアル・アンド・エラーを繰り返す他なかった。

marked.js などのようなパーサはその辺サクッととりまとめてくれているものの、利用者からはパーサの内部が見えないので、拡張構文を扱ったり、HTML 変換に向けて AST を扱ったりするのは大変だった。

そういう意味では、Remark は柔軟なカスタマイズが可能で、特に HTML ファイルを生成するためにはかなり有用だと感じた。

次回は Frontmatter などの拡張構文を解釈できるようにし、HTML 出力もカスタマイズしてみようと思う。