Remark・Rehype を使って Markdown から HTML に変換する
Gatsby.js を使っていて、Markdown ファイルをブログ記事として投稿できる gatsby-transformer-remark
というパッケージを使ってみた。
プラグインを入れていくと、Frontmatter と呼ばれる YAML テンプレート部分を解釈できるようで、同様の仕組みは Hexo という静的サイトジェネレータでも導入されている。
こうした仕組みが面白そうだったので、内部実装を調べてみると、Remark とか Rehype とかいうパッケージが利用されていた。
目次
Remark とは
はじめに、今回紹介するのは、以下の gnab/remark ではない。
- 参考 : Remark
- 参考 : GitHub - gnab/remark: A simple, in-browser, markdown-driven slideshow tool.
- コチラは Markdown からスライドショーを作成できるツール
今回紹介するのは、公式サイト remark.js.org
、remarkjs/remark だ。
- remark - markdown processor powered by plugins
- GitHub - remarkjs/remark: Markdown processor powered by plugins part of the @unifiedjs collective
Remark は、Unified.js という規格に沿って作られた Markdown のパーサで、様々な機能をプラグインで導入できる。
Remark に関する代表的なパッケージは以下の2つ。
remark-parse
: Markdown をパースしてmdast
という AST (抽象構文木) に変換するremark-stringify
:mdast
をパースして Markdown に変換する
上の2つだけ見ると、「Markdown → mdast → Markdown」としか扱えず、HTML 化できないんじゃないの?と思われるだろう。そこで登場するのが Rehype だ。
Rehype とは
Rehype は、Unified.js の規格に沿って作られた HTML のパーサだ。コチラにも以下のようなパッケージがある。
rehype-parse
: HTML をパースしてhast
という AST に変換するrehype-stringify
:hast
をパースして HTML に変換する
コチラもコレだけ見ると、「HTML → hast → HTML」としか扱えない。
Markdown と HTML の相互変換
じゃあ Markdown から HTML に変換するにはどうしたらいいかというと、変換用のパッケージがある。
remark-rehype
: mdast から hast に変換するrehype-remark
: hast から mdast に変換する
つまり、一度 AST に変換して、それから双方の AST へと変換するワケだ。
ということで Markdown から HTML へと変換するには、
remark-parse
で Markdown を mdast に変換するremark-rehype
で mdast から hast に変換する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 をインストールしている。
- 参考 : Standard MarkdownがCommon Markdow、そしてCommonMarkに
- 参考 : remark+ReactでMarkdownをレンダリングする – 踊る犬.netブログ (旧)
- 参考 : How to use pedantic mode With remark v13 (remark-parse v9.0.0)? · Discussion #547 · remarkjs/remark · GitHub
ちなみに、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 出力もカスタマイズしてみようと思う。
- 参考 : Remark で広げる Markdown の世界
- 独自パーサの作り方が丁寧に紹介されているが、remark-parse v9 以降では通用しない古い方式なのが残念…
- 参考 : rehypeを使ってHTMLを書き換える | Developers.IO