Rehype プラグインで HTML の見出し要素にリンクを貼る

GitHub で表示される MarkdownBootstrap のドキュメントなんかで、見出しにカーソルを当てると左右に $ とか # みたいなアイコンが表示されて、コレが該当の見出しへのパーマリンクになっているモノがある。

自分で HTML を書くと、以下のように実装する感じだ。

<h1 id="my-headline">
  <a href="#my-headline">$</a>
  こんにちは見出しです
</h1>

今回は <a href="#my-headline">$</a> この部分を自動的に付与してくれる Rehype プラグイン rehype-autolink-headings を紹介する。

必要なパッケージとサンプルコード

今回は Markdown から HTML にパースする際に、見出しにリンクを付けてみる。Rehype プラグインなので、HTML を読み込んで、見出しにリンクを付けて、再度 HTML にパースし直す、といった使い方もできる。

# Markdown を HTML へと変換するために必要な最低限の Unified・Remark・Rehype 関連パッケージ
$ npm install --save-dev unified remark-parse remark-rehype rehype-stringify

# 見出しへの ID 付与を行う Remark プラグインと、見出しリンクを付与する今回のプラグイン
$ npm install --save-dev remark-slug rehype-autolink-headings

以下のようにパースする。

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

const unified = require('unified');
const remarkParse = require('remark-parse');
const remarkSlug = require('remark-slug');
const remarkRehype = require('remark-rehype');
const rehypeAutolinkHeadings = require('rehype-autolink-headings');
const rehypeStringify = require('rehype-stringify');

(async () => {
  const inputMarkdown = await fs.readFile('./example.md', 'utf-8');
  
  const processor = unified()
    .use(remarkParse)
    .use(remarkSlug)    // 見出しに ID を自動付与する
    .use(remarkRehype)  // Markdown から HTML に変換する
    .use(rehypeAutolinkHeadings, {  // HTML に変換後、見出しリンクを自動付与する
      behavior: 'prepend',          // `prepend`・`append`・`wrap`・`before`・`after` でリンクの挿入位置を指定できる
      properties: {},               // `a` 要素に付与する属性
      content: {                    // hast Node として `a` 要素の子要素を定義できる・今回は span 要素を追加してみる
        type: 'element',
        tagName: 'span',
        properties: {
          className: ['icon-header-link']
        },
        children: [{
          type: 'text',
          value: '$'
        }]
      }
    })
    .use(rehypeStringify);
  const result = await processor.process(inputMarkdown);
  
  const outputHtml = result.contents;
  await fs.writeFile('./example.html', outputHtml, 'utf-8');
})();

コレで、生成される HTML は以下のようになる。インデントだけ調整している。

<h1 id="タイトル">
  <a href="#タイトル"><span class="icon-header-link">$</span></a>
  タイトル
</h1>
<p>文章</p>

<h2 id="はじめに">
  <a href="#はじめに"><span class="icon-header-link">$</span></a>
  はじめに
</h2>
<p>文章…</p>

こんな感じ。

behavior: 'prepend' という指定で、h1 要素などの先頭に a 要素が追加されている。挿入位置はお好みで。

content オプションは hast (HTML AST) の仕様に則って子要素を指定できるモノ。今回は span 要素を内側に入れ、$ というテキストノードを追加している。

こうした見出しリンクの付与が自動化できるのはありがたい。