Next.js のための Remark / Rehype 入門

この記事は [Jamstack Advent Calendar 2020](https://qiita.com/advent-calendar/2020/jamstack) 4日目の記事です。

## はじめに

Markdown -> HTML の変換に使われる事が多い、Remark / Rehype について書いていきます。

検索すると、Gatsby.js のプラグイン関連の記事は結構出てくるのですが、素の Remark / Rehype にフォーカスを当てた記事は少ないので、よく分からないまま使っているという人も多いのではないでしょうか。

## 全体像
実はこれらは、[Unified](https://unifiedjs.com) と呼ばれるエコシステムの一部に属しています。その中でも、

* Markdown を扱う処理系が Remark
* HTML を扱う処理系が Rehype

となっています。 

### 処理の仕組み

その処理の仕組みは、Babel や PostCSS によく似ていて、

1. `Parse` : 文字列を AST に変換する
2. `Transform` : AST に対して何か操作を行う
3. `Stringify` : AST を文字列に戻す

という流れになります。一旦、AST (抽象構文木)と呼ばれる、プログラムで扱いやすい形式を経由することで、Markdown や HTML を柔軟に書き換えることができます。

Unified では、

* [`mdast`](https://github.com/syntax-tree/mdast) : Markdown の AST 
* [`hast`](https://github.com/syntax-tree/hast) : HTML の AST

という仕様が定義されており、一連の変換プロセスの中で、両者が相互変換できるようになっているのも大きなポイントです。
![remark_rehype_overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266232/aaaf9242-9005-4feb-f675-92e13677ef34.png)

### プラグイン制
Unified ではプラグイン制が採用されており、自分で AST を操作するコードを書かなくても、プラグインを使うことで色々できるようになっています。

## プラグインの利用例

* [remark プラグイン一覧](https://github.com/remarkjs/remark/blob/main/doc/plugins.md)
* [rehype プラグイン一覧](https://github.com/rehypejs/rehype/blob/main/doc/plugins.md)

### Markdown を HTML に変換する
#### 実装例 1
1. Markdown を `mdast` に変換
2. `mdast` を `hast` に変換
3. `hast` を HTML に変換

という手順を踏みます。

* [remark-parse](https://www.npmjs.com/package/remark-parse)
* [remark-rehype](https://www.npmjs.com/package/remark-rehype)
* [rehype-stringify](https://www.npmjs.com/package/rehype-stringify)

という３つのプラグインを使って、以下のように書くことができます。

```bash
$ npm install unified remark-parse remark-rehype rehype-stringify
```

```js
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](https://www.npmjs.com/package/remark-parse) と [unified](https://www.npmjs.com/package/unified) の部分を [remark](https://www.npmjs.com/package/remark)
* [remark-rehype](https://github.com/remarkjs/remark-rehype) と [remark-stringify](https://www.npmjs.com/package/remark-stringify) (+サニタイズ処理) の部分を [remark-html](https://github.com/remarkjs/remark-html)

というプラグインに置きかえて、以下のように書くこともできます。
Next.js の Examples 内にある、 [blog-starter](https://github.com/vercel/next.js/tree/master/examples/blog-starter) などでは、こちらの方式が採用されています。

```bash
$ npm install remark remark-html
```

```js
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();
}
```

### 目次を生成する
全ページに自分で目次を作るのは結構大変ですよね。

* [remark-slug](https://github.com/remarkjs/remark-slug)
* [remark-toc](https://github.com/remarkjs/remark-toc)

の２つを使うことで、クリックでその見出しにジャンプできる目次を自動生成できます。

#### 実装例
デフォルトだと、`Table of Contents`, `toc`,  `table-of-contents` という見出しの下に目次が生成されるため、`目次`という見出しの下に生成されるように変更しています。

`use`の第２引数に、オブジェクト形式でオプションを渡すことができます。

```bash
$ npm install remark remark-html remark-slug remark-toc
```

```js
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
```

↓

```html
<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](https://github.com/remarkjs/remark-highlight.js)
* [rehype-highlight](https://github.com/rehypejs/rehype-highlight)
* [remark-prism](https://github.com/sergioramos/remark-prism)
* [rehype-prism](https://github.com/mapbox/rehype-prism)
* [rehype-shiki](https://github.com/rsclarke/rehype-shiki)

などが使えそうです。

本記事では、[remark-highlight.js](https://github.com/remarkjs/remark-highlight.js) を使った実装例を紹介します。

#### 実装例

```bash
$ npm install remark remark-html remark-highlight.js
```

```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');
```

↓

```html
<pre>
  <code class='hljs language-js'>
    <span class='hljs-built_in'>console</span>
    .log(
    <span class='hljs-string'>'Hello'</span>
    );
  </code>
</pre>
```
#### スタイルを当てる

このプラグインはタグの生成 & クラス名の付与をしてくれるだけです。
実際に色をつけるには、[別途テーマのCSS](https://github.com/highlightjs/highlight.js/tree/master/src/styles) を読み込む必要があります。

```bash
$ npm install highlight.js
```

した後、`_app.js` or ページ内で、以下のように CSS を読み込むことで色がつきます。

```js
import 'highlight.js/styles/default.css';
```

## Remark と Rehype の使い分け
プラグインの中には、Remark と Rehype の両方とも存在しているものがあります。

Markdown -> HTML という変換プロセスの場合は、両方を使う余地があるので迷うと思うのですが、基本的に目的の結果が得られれば、どちらを使っても大差は無いのではと思います。

ただ、設定できるオプションが結構違っているケースが多く、Remark 版ではできないことが、Rehype 版だとできる(またはその逆)といった場合もあるようです。

## 参考リンク

* [Remark で広げる Markdown の世界](https://vivliostyle.github.io/vivliostyle_doc/ja/vivliostyle-user-group-vol2/spring-raining/index.html)
* [Remark・Rehype を使って Markdown から HTML に変換する](https://neos21.net/blog/2020/11/11-01.html)
* [Introduction to unified](https://unifiedjs.com/learn/guide/introduction-to-unified/)