gfmtoc(mdtocから名前を変えた)を作っている時にGitHub flavored Markdown (本当はHTML Pipeline)互換の見出しIDをNode.jsで生成する方法を考えていた。GitHubではRubyでサポートされているPOSIX文字クラスをユニコードへ対応させた\p{Word}を使っているので、それを作ればあとは簡単なようだ。

Rubyのソースコードにある正規表現のドキュメントによると、以下のユニコード文字プロパティーのカテゴリーを組み合わせたものが\p{Word}ということのようだ。

つまりこれらを連結した正規表現パターンを作れば良い。まともにやったら日が暮れるので、Node.jsではunicode-7.0.0regenerateの両パッケージを使って生成してやる。

var regenerate = require('regenerate');

var word = regenerate().add(
  require('unicode-7.0.0/properties/Any/code-points')
).remove(
  require('unicode-7.0.0/categories/L/code-points')
).remove(
  require('unicode-7.0.0/categories/M/code-points')
).remove(
  require('unicode-7.0.0/categories/N/code-points')
).remove(
  require('unicode-7.0.0/categories/Pc/code-points')
).remove(
  '-',
  ' '
);

console.log(word.toString());

ここでは否定形が欲しいので、Anyプロパティーをadd()してから上記カテゴリーをremove()していく。最終的には特別視される- もここでついでに削除しておく。gfmtocでは生成結果をファイルに保存したものをライブラリとして使っている。普通はそのまま書けば良い(毎回動的に生成させるととても遅いことには注意が必要)。あとはHTML Pipelineがやっていることをなぞるのみだ。

  1. 小文字に変換
  2. \p{Word}- (半角空白)以外を削除
  3. 半角空白を-に置換
  4. IDが被らないように数字を追加

HTML Pipelineではこのような生成手順になっているようだ。

var h = {};

function genID(s, h) {
  var u = '';
  var word = /.../g;
  s = s.toLowerCase();
  s = s.replace(word, '');
  s = s.replace(/ /g, '-');

  if (h[s] > 0) {
    u = '-' + h[s];
  }

  if (!h[s]) {
    h[s] = 0;
  }

  h[s] += 1;

  return s + u;
}

確認した限りではうまくいっているようだ。id属性の値として使う場合はこのままで問題ないが、href属性の値として使う場合はencodeURIComponent()でURLエンコードしてやることを忘れない方が良い……ような気がするけど、最近のブラウザーはカシコイのでどっちでも良さそう。