Markdown-it の markdown-it-named-headers plugin で custom slugify を設定する


# markdown-it-named-headers plugin について

markdown-it-named-headers は、Markdown の header 要素から id 属性を生成します。
こんな感じで、Markdown の header から id を生成し、HTML へのレンダリング時に追加してくれます。

```html
# Example Header   -->   <h1 id="example-header">Example</h1>
```
* [leff/markdown-it-named-headers](https://github.com/leff/markdown-it-named-headers)

# 残念だったな。やっぱり、Visual Studio Code ネタだ。

Visual Studio Code の Markdown Preview のページ内リンクにおけるアンカーについてです。
このネタは、markdown-it-named-headers plugin についてではなく、Visual Studio Code の Markdown Preview に関わっています。

ページ内のリンクは、HTML anchor tag と id 属性を使って、実現しているが、Markdown の header 要素を HTML にレンダリングする際に id 属性を追加するのが markdown-it-named-headers plugin のお仕事。
何がダメかっていうと、日本語などで書いたヘッダから id を生成できない事。

id がどのようになるか、下記のサンプル Markdown で見てみると、

```md
## test
## さくら
## さくら 桜
## 🌸
```

見事にアルファベット以外の header の id が、見事に欠落していました。

```html
<h2 data-line="5" class="code-line" id="test">test</h2>
<h2 data-line="6" class="code-line" id="">さくら</h2>
<h2 data-line="7" class="code-line" id="">さくら 桜</h2>
<h2 data-line="8" class="code-line" id="">🌸</h2>
```

この理由を調べてみた。

markdown-it-named-headers は、[string.js](https://github.com/jprichardson/string.js) の slug() 関数を使って、header に書かれたテキストを有効な URL スラッグに変換することを期待した作りになってる。

困ってる人はいないのかな？と調べてみると、いた。それなりにいた。

* [string.js の slugify がいけないっぽい？](http://stringjs.com/#methods/slugify)
  * [slugify russian (non-english) words #43](https://github.com/jprichardson/string.js/issues/43)
* [truncate doesn't work with non-latin characters (Japanese) #201](https://github.com/jprichardson/string.js/issues/201)

string.js の slug() は、アクセント付きの Latin 文字をそぎ落とす役目を担っているようだが、Latin 文字以外は考慮されていないっぽいので、該当しないものは全部そぎ落としている。なので、日本語のみでヘッダを書いた場合は、有効な文字が 1 つもないため、結果的に id が空になることがわかりました。

解決策は、どれも、string.js でダメなら、Custom Slugify 書いちゃえば良いんじゃん。だった。。。


## 似たような機能を提供する plugin も調べてみた

他の似たような機能を提供する plugin を調べてみた。

* [rstacruz/markdown-it-named-headings](https://github.com/rstacruz/markdown-it-named-headings)
* [valeriangalliat/markdown-it-anchor](https://github.com/valeriangalliat/markdown-it-anchor) 
* [tylingsoft/markdown-it-github-toc](https://github.com/tylingsoft/markdown-it-github-toc)
* [rstacruz/markdown-it-decorate](https://github.com/rstacruz/markdown-it-decorate)
* [arve0/markdown-it-attrs](https://github.com/arve0/markdown-it-attrs)

下記のような Markdown をサンプルとして用意してみた。

  ```markdown
  ## test
  ## 1234
  ## さくら
  ## さくら 桜
  ## サクラ
  ## 🌸
  ## あいうえうお
  ## 零弌弐参四
  ## 欅坂
  ## 乃木坂
  ```

### [rstacruz/markdown-it-named-headings](https://github.com/rstacruz/markdown-it-named-headings)

* anchor に使える id は、[FGRibreau/node-unidecode](https://github.com/FGRibreau/node-unidecode) で生成してくれる。

id には、下記のように変換された値が入る。

  ```html
  <h2 id="test" data-line="5" class="code-line">test</h2>
  <h2 id="1234" data-line="6" class="code-line">1234</h2>
  <h2 id="sakura" data-line="7" class="code-line">さくら</h2>
  <h2 id="sakura-ying" data-line="8" class="code-line">さくら 桜</h2>
  <h2 id="sakura-1" data-line="9" class="code-line">サクラ</h2>
  <h2 id="" data-line="10" class="code-line">🌸</h2>
  <h2 id="aiueuo" data-line="11" class="code-line">あいうえうお</h2>
  <h2 id="ling-yi-er-can-si" data-line="12" class="code-line">零弌弐参四</h2>
  <h2 id="ju-ban" data-line="13" class="code-line">欅坂</h2>
  <h2 id="nai-mu-ban" data-line="14" class="code-line">乃木坂</h2>
  ```
ASCII transliterations of Unicode text ということで、がんばってる感はある。けど、きつい。

### [valeriangalliat/markdown-it-anchor](https://github.com/valeriangalliat/markdown-it-anchor) 

これも同じような問題を抱えている。

  * [Unable to parse cyrillic characters #10](https://github.com/valeriangalliat/markdown-it-anchor/issues/10)

  ```html
  <h2 id="test" data-line="1" class="code-line">test</h2>
  <h2 id="1234" data-line="2" class="code-line">1234</h2>
  <h2 id="" data-line="3" class="code-line">さくら</h2>
  <h2 id="-2" data-line="4" class="code-line">さくら 桜</h2>
  <h2 id="-3" data-line="5" class="code-line">サクラ</h2>
  <h2 id="-4" data-line="6" class="code-line">🌸</h2>
  <h2 id="-5" data-line="7" class="code-line">あいうえうお</h2>
  <h2 id="-6" data-line="8" class="code-line">零弌弐参四</h2>
  <h2 id="-7" data-line="9" class="code-line">欅坂</h2>
  <h2 id="-8" data-line="10" class="code-line">乃木坂</h2>
  ```

解決策としては、custom slugify を定義すれば良いとなっている。。。ので、こいつは custom slugify が使える。

  * [you couldn't translate Chinese into id property? #17](https://github.com/valeriangalliat/markdown-it-anchor/issues/17) では、[transliteration](https://github.com/andyhu/transliteration) を使って、ascii に変換して対処してる

### [tylingsoft/markdown-it-github-toc](https://github.com/tylingsoft/markdown-it-github-toc)

スルーでした。

  ```html
  <h2 data-line="0" class="code-line" id="test"><a class="markdownIt-Anchor" href="#test">#</a> test</h2>
  <h2 data-line="1" class="code-line" id="1234"><a class="markdownIt-Anchor" href="#1234">#</a> 1234</h2>
  <h2 data-line="2" class="code-line" id="さくら"><a class="markdownIt-Anchor" href="#さくら">#</a> さくら</h2>
  <h2 data-line="3" class="code-line" id="さくら-桜"><a class="markdownIt-Anchor" href="#さくら-桜">#</a> さくら 桜</h2>
  <h2 data-line="4" class="code-line" id="サクラ"><a class="markdownIt-Anchor" href="#サクラ">#</a> サクラ</h2>
  <h2 data-line="5" class="code-line" id="a"><a class="markdownIt-Anchor" href="#a">#</a> 🌸</h2>
  <h2 data-line="6" class="code-line" id="あいうえうお"><a class="markdownIt-Anchor" href="#あいうえうお">#</a> あいうえうお</h2>
  <h2 data-line="7" class="code-line" id="零弌弐参四"><a class="markdownIt-Anchor" href="#零弌弐参四">#</a> 零弌弐参四</h2>
  <h2 data-line="8" class="code-line" id="欅坂"><a class="markdownIt-Anchor" href="#欅坂">#</a> 欅坂</h2>
  <h2 data-line="9" class="code-line" id="乃木坂"><a class="markdownIt-Anchor" href="#乃木坂">#</a> 乃木坂</h2>
  ```

  * [uslug](https://github.com/jeremys/uslug) を利用してる
  * なんか、そのまま id にセットされる
  * slugify の処理は uslug を通しているだけ
  * 独自に slugify の処理を定義することはできない

### [rstacruz/markdown-it-decorate](https://github.com/rstacruz/markdown-it-decorate)

HTML のコメント tag を使って、好きな id を割り当てられるような作りになっていた。

  * slugify などの考え方はなく、`<!-- {#id} -->` のように書くことで id を追加できる


### [arve0/markdown-it-attrs](https://github.com/arve0/markdown-it-attrs)

rstacruz/markdown-it-decorate と似たような感じで、 `## さくら {#さくら}` のような書き方をすることで好きな id を設定できる。独自に slugify の処理を定義することはできない。

```html
<h2 id="test" data-line="7" class="code-line" id="test">test</h2>
<h2 id="1234" data-line="8" class="code-line" id="1234">1234</h2>
<h2 id="さくら" data-line="9" class="code-line" id="">さくら</h2>
<h2 id="さくら" 桜="" data-line="10" class="code-line" id="">さくら 桜</h2>
<h2 id="サクラ" data-line="11" class="code-line" id="">サクラ</h2>
<h2 id="桜" data-line="12" class="code-line" id="">🌸</h2>
<h2 id="あいうえうお" data-line="13" class="code-line" id="">あいうえうお</h2>
<h2 id="零弌弐参四" data-line="14" class="code-line" id="">零弌弐参四</h2>
<h2 id="欅坂" data-line="15" class="code-line" id="">欅坂</h2>
<h2 id="乃木坂" data-line="16" class="code-line" id="">乃木坂</h2>
```

## ここまでまとめてみると

闇でした。

Markdown-it の issues でも、話題になっていてヒントもあった。が、こちらも、これといった解決方法はまだないっぽく Issue は Open のままだった。(でも、真っ先にこれに気が付きたかった・・・

* [Header anchors [needs discussion] #28](https://github.com/markdown-it/markdown-it/issues/28)


## どうすれば良いの？

markdown-it-named-headers をそのまま使うことを前提に、custom slugify を自分で書く。

また、header の処理部分に言及していたけど、それへリンクを張るアンカー側はどうなっているかを確認する必要もある。

Visual Studio Code の Markdown Preview では、アンカーにアルファベット以外のものが設定されると `encodeURI()` してから `<a href>` タグを追加する処理が組み込まれていた。

こんな感じになる: 

```html
<!--
* [test](#test)
* [さくら](#さくら)
* [さくら 桜](#さくら-桜)
* [🌸](#🌸)
-->

<li data-line="0" class="code-line"><a href="#test">test</a></li>
<li data-line="1" class="code-line"><a href="#%E3%81%95%E3%81%8F%E3%82%89">さくら</a></li>
<li data-line="2" class="code-line"><a href="#%E3%81%95%E3%81%8F%E3%82%89-%E6%A1%9C">さくら 桜</a></li>
<li data-line="3" class="code-line"><a href="#%F0%9F%8C%B8">🌸</a></li>
```
おお。どうして、ここまで実装していて気が付かなかったんだと問い(省略)

ここで、アンカーを書くときのルールをまとめてみた。

* アンカーは、必ず小文字で書く
* スペースは `-` に変換する
* ```!@#$%^&*()_+={}][|\"':;?/>.<,`~``` の文字は、削除される

| ヘッダ        | リンク            | アンカー (生成される href) |
|--------------|------------------|-------------------------|
| ## test      | (#test)          | #test |
| ## TEST-2あ  | (#test-2あ)      | #test-2%E3%81%82 |
| ## さくら     | (#さくら)        | #%E3%81%95%E3%81%8F%E3%82%89 |
| ## さくら 桜  | (#さくら-桜)      | #%E3%81%95%E3%81%8F%E3%82%89-%E6%A1%9C |
| ## 🌸        | (#🌸)           | #%F0%9F%8C%B8 |
| ## 🌸 🌸      | (#🌸-🌸)       | #%F0%9F%8C%B8-%F0%9F%8C%B8 |
| ## test      | (TEST test \\\\) | #test-test-test |
| ## a........ | (#a)             | #a |
| ## ´¨ˆ˜      | (#´¨ˆ˜)          | #%C2%B4%C2%A8%CB%86%CB%9C |

* 文字列の decode/encode を試してみるなら、<http://www.urldecoder.org> あたりで

ということは、これをそのまま使うためにも、同じ結果を得る必要があることがわかりました。

期待することは、こんな感じ: 

```html
<!--
## test
## さくら
## さくら 桜
## 🌸
-->

<h2 data-line="5" class="code-line" id="test">test</h2>
<h2 data-line="6" class="code-line" id="%E3%81%95%E3%81%8F%E3%82%89">さくら</h2>
<h2 data-line="7" class="code-line" id="%E3%81%95%E3%81%8F%E3%82%89-%E6%A1%9C">さくら 桜</h2>
<h2 data-line="8" class="code-line" id="%F0%9F%8C%B8">🌸</h2>
```

同じ文字列を手に入れて encodeURI() してあげる必要があることがわかりました。


## custom slugify を書く

custom slugify を自分で書くことになったけど、どう書いて良いかわからず、まずは、URL として利用できる/できない文字を確認してみる

* RFC2396 と RFC3986 に URL で利用できる文字列について言及されている
* [URLで使用可能な文字、使用できない文字](https://www.ipentec.com/document/document.aspx?page=web-url-invalid-char)

RFC で定義されているっぽいので、slug する際は、下記のルールを適用することにしてみる。

* header に書かれているテキストは、まず、trim() して toLowerCase() で小文字に変換する 
* URL に使えない文字となる記号を replace() で削除
* スペースは、replace() で `-` に変換
* おしりに `-` がつくなら replace() で削除
* 最後に生き残った文字列を encodeURI() でエンコードして返す
* これが id にセットされる

正規表現を確認するために vscode の拡張機能 [Regex Previewer](https://marketplace.visualstudio.com/items?itemName=chrmarti.regex) にお世話になりまくりながら、出来上がった custom slugify がこちら。

```ts
.use(mdnh, {
	slugify: function (header: string) {
		return encodeURI(header.trim()
 			.toLowerCase()
			.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
			.replace(/\s+/g, '-')) // Replace spaces with hyphens
			.replace(/\-+$/, ''); // Replace trailing hyphen
	  }
})
```

custome slugify でレンダリングされる HTML は下記のようになりました。 

```html
<li data-line="0" class="code-line"><a href="#test">test</a></li>
<li data-line="1" class="code-line"><a href="#%E3%81%95%E3%81%8F%E3%82%89">さくら</a></li>
<li data-line="2" class="code-line"><a href="#%E3%81%95%E3%81%8F%E3%82%89-%E6%A1%9C">さくら 桜</a></li>
<li data-line="3" class="code-line"><a href="#%F0%9F%8C%B8">🌸</a></li>
</ul>
<h2 data-line="5" class="code-line" id="test">test</h2>
<h2 data-line="6" class="code-line" id="%E3%81%95%E3%81%8F%E3%82%89">さくら</h2>
<h2 data-line="7" class="code-line" id="%E3%81%95%E3%81%8F%E3%82%89-%E6%A1%9C">さくら 桜</h2>
<h2 data-line="8" class="code-line code-active-line" id="%F0%9F%8C%B8">🌸</h2>
```

実際の動きは、こうなります。期待どおりです。

![anchor.gif](https://qiita-image-store.s3.amazonaws.com/0/65800/e37fef47-e3ba-6f42-dda0-181cf15bef16.gif "anchor.gif")


## まとめたので

ここまでやったので、思い切って Issues をあげつつ、Pull Request してみたところ、サクッと merge して貰えました。やったね。

* Issues: [non-latin characters (such as japanese) and Markdown preview anchor #20626](https://github.com/Microsoft/vscode/issues/20626)
* Pull Requst: [markdown-it-named-header custom slugify for non-latin characters #20628](https://github.com/Microsoft/vscode/pull/20628)

その後、[Use same slugify logic for editor links as well](https://github.com/Microsoft/vscode/commit/4f6efb0ef5f6d40ced3d12e3c889217c46e77473) で Markdown Extension 本体の TableOfContentProvider が提供する slugify が修正され、markdown-it-named-headers の custom slugify として参照されるように修正が入っています。 

## まとめ

Visual Studio Code insiders あるいは、1.10.0 (Feb Release) 以降、ページ内で header のリンクをを書くときのルールをもう一度まとめてみました。

* ```!@#$%^&*()_+={}][|\"':;?/>.<,`~``` の文字は、削除される
* アルファベットは、必ず小文字で書く
* header に日本語などアルファベット以外の文字列が入っているなら、そのままそれを書く
* スペースは `-` に変換する

大きなドキュメントで試してみたく、[ayatokura/JP-VSCode-Docs/release-notes/v1_8_ja.md](https://github.com/ayatokura/JP-VSCode-Docs/blob/master/release-notes/v1_8_ja.md) で公開されている、Visual Studio Code 1.8 のリリースノート日本語訳を使ってテストしてみましたが、うまく動いているようです。

![anchor_test.gif](https://qiita-image-store.s3.amazonaws.com/0/65800/a7d4f15f-ef2b-7517-7009-a6ec0c9f7468.gif "anchor_test.gif")


## おまけ: Custom Slugify を利用できる plugin

* [leff/markdown-it-named-headers](https://github.com/leff/markdown-it-named-headers) (vscode で利用されているる)
* [valeriangalliat/markdown-it-anchor](https://github.com/valeriangalliat/markdown-it-anchor)