markdown-it-named-headers plugin について
markdown-it-named-headers は、Markdown の header 要素から id 属性を生成します。
こんな感じで、Markdown の header から id を生成し、HTML へのレンダリング時に追加してくれます。
# Example Header --> <h1 id="example-header">Example</h1>
残念だったな。やっぱり、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 で見てみると、
## test
## さくら
## さくら 桜
## 🌸
見事にアルファベット以外の header の id が、見事に欠落していました。
<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 の slug() 関数を使って、header に書かれたテキストを有効な URL スラッグに変換することを期待した作りになってる。
困ってる人はいないのかな?と調べてみると、いた。それなりにいた。
string.js の slug() は、アクセント付きの Latin 文字をそぎ落とす役目を担っているようだが、Latin 文字以外は考慮されていないっぽいので、該当しないものは全部そぎ落としている。なので、日本語のみでヘッダを書いた場合は、有効な文字が 1 つもないため、結果的に id が空になることがわかりました。
解決策は、どれも、string.js でダメなら、Custom Slugify 書いちゃえば良いんじゃん。だった。。。
似たような機能を提供する plugin も調べてみた
他の似たような機能を提供する plugin を調べてみた。
- rstacruz/markdown-it-named-headings
- valeriangalliat/markdown-it-anchor
- tylingsoft/markdown-it-github-toc
- rstacruz/markdown-it-decorate
- arve0/markdown-it-attrs
下記のような Markdown をサンプルとして用意してみた。
## test
## 1234
## さくら
## さくら 桜
## サクラ
## 🌸
## あいうえうお
## 零弌弐参四
## 欅坂
## 乃木坂
rstacruz/markdown-it-named-headings
- anchor に使える id は、FGRibreau/node-unidecode で生成してくれる。
id には、下記のように変換された値が入る。
<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
これも同じような問題を抱えている。
<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 では、transliteration を使って、ascii に変換して対処してる
tylingsoft/markdown-it-github-toc
スルーでした。
<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 を利用してる
- なんか、そのまま id にセットされる
- slugify の処理は uslug を通しているだけ
- 独自に slugify の処理を定義することはできない
rstacruz/markdown-it-decorate
HTML のコメント tag を使って、好きな id を割り当てられるような作りになっていた。
- slugify などの考え方はなく、
<!-- {#id} -->
のように書くことで id を追加できる
arve0/markdown-it-attrs
rstacruz/markdown-it-decorate と似たような感じで、 ## さくら {#さくら}
のような書き方をすることで好きな id を設定できる。独自に slugify の処理を定義することはできない。
<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 のままだった。(でも、真っ先にこれに気が付きたかった・・・
どうすれば良いの?
markdown-it-named-headers をそのまま使うことを前提に、custom slugify を自分で書く。
また、header の処理部分に言及していたけど、それへリンクを張るアンカー側はどうなっているかを確認する必要もある。
Visual Studio Code の Markdown Preview では、アンカーにアルファベット以外のものが設定されると encodeURI()
してから <a href>
タグを追加する処理が組み込まれていた。
こんな感じになる:
<!--
* [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 あたりで
ということは、これをそのまま使うためにも、同じ結果を得る必要があることがわかりました。
期待することは、こんな感じ:
<!--
## 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で使用可能な文字、使用できない文字
RFC で定義されているっぽいので、slug する際は、下記のルールを適用することにしてみる。
- header に書かれているテキストは、まず、trim() して toLowerCase() で小文字に変換する
- URL に使えない文字となる記号を replace() で削除
- スペースは、replace() で
-
に変換 - おしりに
-
がつくなら replace() で削除 - 最後に生き残った文字列を encodeURI() でエンコードして返す
- これが id にセットされる
正規表現を確認するために vscode の拡張機能 Regex Previewer にお世話になりまくりながら、出来上がった custom slugify がこちら。
.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 は下記のようになりました。
<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>
実際の動きは、こうなります。期待どおりです。
まとめたので
ここまでやったので、思い切って Issues をあげつつ、Pull Request してみたところ、サクッと merge して貰えました。やったね。
- Issues: non-latin characters (such as japanese) and Markdown preview anchor #20626
- Pull Requst: markdown-it-named-header custom slugify for non-latin characters #20628
その後、Use same slugify logic for editor links as well で 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 で公開されている、Visual Studio Code 1.8 のリリースノート日本語訳を使ってテストしてみましたが、うまく動いているようです。
おまけ: Custom Slugify を利用できる plugin
- leff/markdown-it-named-headers (vscode で利用されているる)
- valeriangalliat/markdown-it-anchor