この記事は Jamstack Advent Calendar 2020 4日目の記事です。
はじめに
Markdown -> HTML の変換に使われる事が多い、Remark / Rehype について書いていきます。
検索すると、Gatsby.js のプラグイン関連の記事は結構出てくるのですが、素の Remark / Rehype にフォーカスを当てた記事は少ないので、よく分からないまま使っているという人も多いのではないでしょうか。
全体像
実はこれらは、Unified と呼ばれるエコシステムの一部に属しています。その中でも、
- Markdown を扱う処理系が Remark
- HTML を扱う処理系が Rehype
となっています。
処理の仕組み
その処理の仕組みは、Babel や PostCSS によく似ていて、
-
Parse
: 文字列を AST に変換する -
Transform
: AST に対して何か操作を行う -
Stringify
: AST を文字列に戻す
という流れになります。一旦、AST (抽象構文木)と呼ばれる、プログラムで扱いやすい形式を経由することで、Markdown や HTML を柔軟に書き換えることができます。
Unified では、
という仕様が定義されており、一連の変換プロセスの中で、両者が相互変換できるようになっているのも大きなポイントです。
プラグイン制
Unified ではプラグイン制が採用されており、自分で AST を操作するコードを書かなくても、プラグインを使うことで色々できるようになっています。
プラグインの利用例
Markdown を HTML に変換する
実装例 1
- Markdown を
mdast
に変換 -
mdast
をhast
に変換 -
hast
を HTML に変換
という手順を踏みます。
という3つのプラグインを使って、以下のように書くことができます。
$ npm install unified remark-parse remark-rehype rehype-stringify
import unified from 'unified';
import remarkParse from 'remark-parse';
import remark2rehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
// Markdown 形式の文字列を受け取って、HTML 形式の文字列を返す
export default async function markdownToHtml(markdown) {
const result = await unified()
.use(remarkParse) // markdown -> mdast の変換
.use(remark2rehype) // mdast -> hast の変換
.use(rehypeStringify) // hast -> html の変換
.process(markdown); // 実行
return result.toString();
}
実装例 2
先ほどのコードは、
- remark-parse と unified の部分を remark
- remark-rehype と remark-stringify (+サニタイズ処理) の部分を remark-html
というプラグインに置きかえて、以下のように書くこともできます。
Next.js の Examples 内にある、 blog-starter などでは、こちらの方式が採用されています。
$ npm install remark remark-html
import remark from 'remark';
import html from 'remark-html';
export default async function markdownToHtml(markdown) {
const result = await remark().use(html).process(markdown);
return result.toString();
}
目次を生成する
全ページに自分で目次を作るのは結構大変ですよね。
の2つを使うことで、クリックでその見出しにジャンプできる目次を自動生成できます。
実装例
デフォルトだと、Table of Contents
, toc
, table-of-contents
という見出しの下に目次が生成されるため、目次
という見出しの下に生成されるように変更しています。
use
の第2引数に、オブジェクト形式でオプションを渡すことができます。
$ npm install remark remark-html remark-slug remark-toc
import remark from 'remark';
import html from 'remark-html';
import slug from 'remark-slug';
import toc from 'remark-toc';
export default async function markdownToHtml(markdown) {
const result = await remark()
.use(slug)
.use(toc, { heading: '目次', maxDepth: 2 })
.use(html)
.process(markdown);
return result.toString();
}
実行結果
## 目次
## aaa
## bbb
↓
<h2 id="目次">目次</h2>
<ul>
<li><a href="#aaa">aaa</a></li>
<li><a href="#bbb">bbb</a></li>
</ul>
<h2 id="aaa">aaa</h2>
<h2 id="bbb">bbb</h2>
シンタックスハイライトをつける
などが使えそうです。
本記事では、remark-highlight.js を使った実装例を紹介します。
実装例
$ npm install remark remark-html remark-highlight.js
import remark from 'remark';
import html from 'remark-html';
import highlight from 'remark-highlight.js';
export default async function markdownToHtml(markdown) {
const result = await remark()
.use(highlight)
.use(html)
.process(markdown);
return result.toString();
}
実行結果
コードブロックに hljs-built_in
, hljs-string
といったクラス名が付与されている事が分かります。
```js
console.log('Hello');
↓
<pre>
<code class='hljs language-js'>
<span class='hljs-built_in'>console</span>
.log(
<span class='hljs-string'>'Hello'</span>
);
</code>
</pre>
スタイルを当てる
このプラグインはタグの生成 & クラス名の付与をしてくれるだけです。
実際に色をつけるには、別途テーマのCSS を読み込む必要があります。
$ npm install highlight.js
した後、_app.js
or ページ内で、以下のように CSS を読み込むことで色がつきます。
import 'highlight.js/styles/default.css';
Remark と Rehype の使い分け
プラグインの中には、Remark と Rehype の両方とも存在しているものがあります。
Markdown -> HTML という変換プロセスの場合は、両方を使う余地があるので迷うと思うのですが、基本的に目的の結果が得られれば、どちらを使っても大差は無いのではと思います。
ただ、設定できるオプションが結構違っているケースが多く、Remark 版ではできないことが、Rehype 版だとできる(またはその逆)といった場合もあるようです。