見出しに id と permalink を付けた #
見出しの右に # リンクを付けた(issue #69)。この記事の見出しにも付いているはずなので、試しに長押しや右クリックしてみてほしい。スマホだと hover が効かないので、常時表示の # を選んでいる。GitHub 流の「hover したら出る」はモバイルで届かない。
なんで今更 #
prev / next で記事同士を繋いだとき、#見出し へのフラグメントを書ける道は以前からあった。ところが飛び先がなかった。tinymarkdown の Section は <section> と <hN> を出すだけで、どこにも id がなかったのだ。id を振って、それをコピーできる入口も欲しかった。
id の決め方、案の比較 #
最初に 5 案を整理した。
- **GitHub 風 slug**: タイトルを小文字化して空白を
-に、記号を除去。MoonBit の話→moonbit-の話。読めるし意味を持つ。 - **ASCII だけの slug**: non-ASCII を落とす。このブログは日本語の見出しが主体なので、空の id が続出すると見込んで却下。
- **連番**
sec-1など: 実装は一番軽いが、見出しの挿入で番号がずれてリンクが壊れる。 - **ハッシュ**: 一意だが URL が読めない。
- **
{#id}記法**: 一番安定。だが tinymarkdown の構文拡張が要り、3 実装+テストのミラーが全部増える。
{#id} は「翻訳間でも id を保つ」なら本命なのだけど、今のところこのブログは 1 言語しか書いていない。機能拡張として Future work に回して、まずは自動採番を入れた。
GitHub 風のつもりが GitLab 式になった #
ここが紆余曲折その一。「github-slugger 互換」を掲げたのだが、実装に入って気づいた。github-slugger 本体は、除去対象文字を巨大な正規表現に列挙したもので、それを Go / MoonBit / TypeScript の 3 言語に写すのは正確さも保守も厳しい。しかも MoonBit の Char には Unicode の小文字化も is_alphabetic もなく、is_numeric / is_whitespace / ASCII 判定しかない。
そこで逆に、残すものを定義する GitLab 17.0 以降の式に寄せた。「文字・数字・-・_ を残し、それ以外の記号・絵文字を落とす」。Unicode のカテゴリー判定が使えない MoonBit でも書けるよう、落とす文字の範囲テーブル(絵文字・CJK 句読点・罫線・矢印・ラテン1補助記号など)を 1 枚持って、3 実装が同じ表をなめる形にした。結果的に「文字種で決める GitLab 式」であり、通常の見出しでは GitHub と結果が変わらない。小文字化も ASCII と全角 A-Z だけに絞って、3 言語で完全に同じルールにした。
空白は除去じゃなくて - 変換だった #
紆余曲折その二。自分の整理では「空白も記号と一緒に除去」と書いていたのだが、GitHub の実際は空白を - に変換する。指摘を受けて直した。変換対象は Unicode の空白全般(全角スペースも)。連続空白は潰さず a b → a--b になる、これも GitHub に合わせた。
細かいところで、JavaScript の \s は Unicode の White_Space と微妙に集合が違う(U+0085 を含まず U+FEFF を含む)。Go の unicode.IsSpace と MoonBit の is_whitespace は White_Space 準拠なので、TS 版だけは正規表現に頼らず範囲で書いて揃えた。
その他の決定 #
- **対象は h1〜h5 全部**: 当初「とりあえず h3 まで」だったが、絞る理由も薄かったので全部にした。
- **絵文字は除去**:
😀 Emojiは-emojiになる。先頭の-が残るのは GitHub 互換。 - **重複は
-1,-2**: dedup も github-slugger と同じで、生成した id も登録する。fooが 2 回現れてfoo,foo-1になったあと、タイトルがそのままfoo-1の見出しが来るとfoo-1-1になる。 - **id は
<hN>に**:<section>ではなく、GitHub と同じく見出し側に。 - **パーマリンクの
#は Raw ノード**: テキスト描画(メタ情報用の plain text 変換)が拾わないので、タイトルの整形が崩れない。
レビューで拾われた抜け #
Devin Review が、front matter だけでタイトルが付くページを指摘した。本文に見出しがない場合、タイトルを補完して <h1> を出す経路があり、そこは id もリンクも付いていなかった。ページの一番上、一番リンクしたい場所なので対応して、3 実装とも補完 h1 も同じ gen で採番するようにした。docs/notes のようなファイル名タイトルでも id が届く。
確認 #
いつも通り Go / MoonBit / ts で規則をミラーし、slugify のテストケースも 3 実装で同じものを回している。最後に Go サーバーと wasm ビルドの実ページで本文を総当たり diff して、バイト一致を見た。