14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-02-19

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 を調べてみた。

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

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

rstacruz/markdown-it-named-headings

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 が使える。

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

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

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

<!--
## 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 として利用できる/できない文字を確認してみる

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>

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

anchor.gif

まとめたので

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

その後、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 のリリースノート日本語訳を使ってテストしてみましたが、うまく動いているようです。

anchor_test.gif

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

14
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?