Slug 化による見出し項目からアンカー ID への変換
Qiita や GitHub の Markdown における “見出し項目からアンカー ID への変換” は Slugify(本記事では Slug 化 と呼称)と呼ばれる。
この Slug 化はそれぞれ次のリポジトリで確認することが出来る。
- Qiita Markdown のリポジトリ:
qiita-markdown/heading_anchor.rb at master · increments/qiita-markdown - GitHub Markdown のリポジトリ:
html-pipeline/toc_filter.rb at main · gjtorikian/html-pipeline
これらを参照することで、どのような処理が成されているか分かる。ともに Ruby で構築されており、[\p{Word}\- ] 以外を無視し、半角スペースを - に置き替えている。
この処理に関して、いくつかのケースに注目して具体的に考えたい。
■ 空白文字
半角スペースは - に置き替えられる。一方で、これ以外の空白文字は無視される。
また、半角スペースであっても見出し末尾の半角スペースは無視される。したがって、## Heading␣␣␣ (“Heading”の後に半角スペースが 3 つある)のような見出しがあったとしても、アンカー ID は #heading--- とはならず #heading となる。
■ 無視される文字
アンカー ID で利用できる文字は \p{Word} だった。これを Unicode 一般カテゴリで表現することで、以下の表に示す Unicode カテゴリ 11 種と 133 文字であることが分かる。
この一般カテゴリでの表現については以下の記事を参照してほしい。
| Unicode 一般カテゴリ(11 種) | 133 文字 |
|---|---|
これより、Slug 化によって無視される Unicode 一般カテゴリは、上の表にある Unicode カテゴリを除いけば良い。
正直言って非常に分かりづらいが、感覚的には発音される文字と数字のみがアンカー ID として利用できる。逆に、句読点や括弧類(約物)、記号、組文字、Unicode 絵文字は Slug 化によって無視される。
!注意の必要な文字
アンカー ID に利用できる Unicode カテゴリには Connector Punctuation が含まれており、このカテゴリには _ や _ が含まれている。そのため、file_1 を見出しに含む場合には _ を無視してはならない。一方で、強調として _ を利用する場合は無視される。
数字に関して、Letter Number に含まれるローマ数字 (e.g. Ⅱ) は無視されず、Other Number に含まれる丸数字 (e.g. ②) は無視される。この辺りは少し覚えておいた方が良さそうである。
波ダッシュ(〜)として利用される WAVE DASH (Dash Punctuation) や FULLWIDTH TILDE (Math Symbol) はともに無視される。そのため、次のような見出しのアンカーリンクを作成する場合は、注意が必要になる。
## ごいす〜な見出し
↓
[ごいす〜な見出し](#ごいすな見出し)
一方で、直線の長音符(ー)は Modifier Letter に含まれているため、無視する必要はない。
この他にも、中黒 ・ (Other Punctuation) や海外の人名などに利用される二重ハイフン ゠ (Dash Punctuation) も無視される。
## ジャン゠ジャック・ルソー
↓
[ジャン゠ジャック・ルソー](#ジャンジャックルソー)
■ 大文字から小文字への置き替え
アルファベットは大文字から小文字に置き替えられる。この置き替えの対応関係は Unicode における UnicodeData.txt に記載されている。(case mapping)
これは、ダイアクリティカルマークの付いたアルファベットに関しても大文字から小文字に置き替えられる。
## APPLE
↓
[APPLE](#apple)
## LEMON
↓
[LEMON](#lemon)
英語に限らず、ギリシャ文字などにも適用されている。
## ΕΛΛΗΝΙΚΆ
↓
[ΕΛΛΗΝΙΚΆ](#ελληνικά)
▽ 数学記号
ギリシャ文字 Σ や Π(ともに Uppercase Letter)に関して少し注意が必要になる。これは、数学記号における総和記号 (∑) や相乗記号 (∏) (ともに Math Symbol)と形が似ているためである。
## ギリシャ文字 Σ Π
[ギリシャ文字](#ギリシャ文字-σ-π)
## 総和記号 (∑) と相乗記号 (∏)
[総和記号と相乗記号](#総和記号--と相乗記号-)
ちなみに、ギリシャ文字 Σ は ς ではなく常に σ に置き替えられる。
▽ 置き替えられないラテン文字
ボールドやイタリック、サンセリフ体などの Mathematical な文字(Lowercase Letter または Uppercase Letter)は、そのままアンカー ID に書き下す。これは、Unicode における UnicodeData.txt に対応の記載がないためである。(𝕡𝕚𝕝𝕚𝓪𝓹𝓹 などで簡単に得ることが出来る)
対象となる Mathematical な文字(折りたたみ)
- セリフ体
- MATHEMATICAL BOLD
- MATHEMATICAL ITALIC
- MATHEMATICAL BOLD ITALIC
- サンセリフ体
- MATHEMATICAL SANS-SERIF
- MATHEMATICAL SANS-SERIF BOLD
- MATHEMATICAL SANS-SERIF ITALIC
- MATHEMATICAL SANS-SERIF BOLD ITALIC
- カリグラフィー体
- MATHEMATICAL SCRIPT
- MATHEMATICAL BOLD SCRIPT
- フラクトゥール
- MATHEMATICAL FRAKTUR
- MATHEMATICAL BOLD FRAKTUR
- MATHEMATICAL DOUBLE-STRUCK
- MATHEMATICAL MONOSPACE
## 𝐇𝑂𝑵𝖤𝗬𝘋𝙀𝒲 𝓜𝔈𝕷𝕆𝙽
↓
[ハネジューメロン](#𝐇𝑂𝑵𝖤𝗬𝘋𝙀𝒲-𝓜𝔈𝕷𝕆𝙽)
また、Mathematical ではない SCRIPT CAPITAL であっても小文字に置き替えられない。(この文字にはそもそも SCRIPT SMALL において、ラテン文字すべてが存在しない)
▽ ローマ数字
アラビア数字には大文字と小文字の区別がないため置き替えられないが、ローマ数字には大文字から小文字に置き替える必要がある。
## Roman Numeral Ⅱ
↓
[ローマ数字](#roman-numeral-ⅱ)
▽ CIRCLED LATIN LETTER
Other Symbol に属している以下の文字の内、CIRCLED LATIN CAPITAL LETTER は小文字に置き替える必要がある。UnicodeData.txt によって対応が定義されている。
## ⒸⒶⓅ
↓
[CIRCLED LATIN LETTER](#ⓒⓐⓟ)
ここで重要になるのは、Copyright の意味で使用する Ⓒ や Ⓟ (ともに CIRCLED LATIN CAPITAL LETTER)である。これらは小文字に置き替える必要がある。
一方で、正しい Copyright は © (COPYRIGHT SIGN) と ℗ (SOUND RECORDING COPYRIGHT) であり、これらは純粋な (?) Other Symbol なので無視される。
■ ダイアクリティカルマーク
ダイアクリティカルマーク(◌̈ など)を含むアルファベットの場合、Unicode 正規化の有無によって、異なる文字と認識される。そのため、見出し項目に利用した文字を忠実に再現する必要がある。
Unicode 正規化されたシュレディンガーの猫
## Schrödinger's cat
↓
[ö: `U+00F6`](#schrödingers-cat)
----------
Unicode 正規化されていないシュレディンガーの猫
## Schrödinger's cat
↓
[ö: `U+006F`+`U+0308`](#schrödingers-cat)
見た目には同じに思えてしまうが、異なる文字列と見なされるため -1 などの番号付けは行われない。
このため、例えば 呪われたテキストジェネレータ で得られるようなダイアクリティカルマークだらけの見出しに対しても、アンカー ID では忠実に再現する必要がある。
## H̞̠̯̰͍̹͆ͮ̈́ẻ̜̝̦̭̘̭̏ͯͭ͑̋̀a͎͖ͧͨ͟͜ḑ̯͇̬͋ͧ͞i̷̷̠̩̍ͤ̓̈́ͩͬ͐͞n̪̭̞ͩ̏͜g̞̬̣̗͚̦̟͈ͥͤͥ̓ͭ̓́ͧ
[呪いの見出し](#h̞̠̯̰͍̹͆ͮ̈́ẻ̜̝̦̭̘̭̏ͯͭ͑̋̀a͎͖ͧͨ͟͜ḑ̯͇̬͋ͧ͞i̷̷̠̩̍ͤ̓̈́ͩͬ͐͞n̪̭̞ͩ̏͜g̞̬̣̗͚̦̟͈ͥͤͥ̓ͭ̓́ͧ)
■ いくつかの絵文字
すべての絵文字が Other Symbol に属しているわけではない。Lowercase Letter に属している絵文字や Decimal Number や囲みラテン文字に異体字セレクタが付いた絵文字がある。⧉
ここで現れる U+20E3 は Enclosing Mark、異体字セレクタの U+FE0F は Nonspacing Mark に属している。そのため、アンカー ID として利用される。
特に囲みラテン文字は、異体字セレクタの有無を見た目から判断することはほとんど不可能なので、注意が必要になる。
- Lowercase Letter
-
INFORMATION ⧉ (ℹ️)
コードポイント 文字 Unicode 名 U+2139ℹINFORMATION SOURCE U+FE0FN/A VARIATION SELECTOR-16
-
INFORMATION ⧉ (ℹ️)
- Decimal Number
-
DIGIT NUMBER ⧉ (Emoji Variation)
コードポイント 文字 Unicode 名 U+0030-U+0039[0-9]DIGIT ZERO - DIGIT NINE U+FE0FN/A VARIATION SELECTOR-16 -
KEYCAP DIGIT NUMBER ⧉ (Emoji Combining Sequence)
コードポイント 文字 Unicode 名 U+0030-U+0039[0-9]DIGIT ZERO - DIGIT NINE U+FE0FN/A VARIATION SELECTOR-16 U+20E3⃣COMBINING ENCLOSING KEYCAP
-
DIGIT NUMBER ⧉ (Emoji Variation)
-
NEGATIVE SQUARED LATIN CAPITAL LETTER (Emoji Variation)
-
BLOOD TYPE ⧉ (🅰️🅱🅾️️)
コードポイント 文字 Unicode 名 U+1F170、U+1F171、U+1F17E[🅰🅱🅾]NEGATIVE SQUARED LATIN CAPITAL LETTER A,B,O U+FE0FN/A VARIATION SELECTOR-16
-
BLOOD TYPE ⧉ (🅰️🅱🅾️️)
-
NEGATIVE SQUARED LATIN CAPITAL LETTER P (Emoji Variation)
-
P BOTTOM ⧉ (🅿️)
コードポイント 文字 Unicode 名 U+1F17F🅿NEGATIVE SQUARED LATIN CAPITAL LETTER P U+FE0FN/A VARIATION SELECTOR-16
-
P BOTTOM ⧉ (🅿️)
-
CIRCLED LATIN CAPITAL LETTER M (Emoji Variation)
-
CIRCLED M ⧉ (Ⓜ️)
コードポイント 文字 Unicode 名 U+24C2ⓂCIRCLED LATIN CAPITAL LETTER M U+FE0FN/A VARIATION SELECTOR-16
-
CIRCLED M ⧉ (Ⓜ️)
▽ 異体字セレクタ
異体字セレクタはアンカー ID に利用される。すでに示したように、上のような絵文字には異体字セレクタがあるかないかを判断して、アンカー ID を作成する必要がある。
一方で、絵文字そのものは無視されるが、異体字セレクタのみがアンカー ID として利用される場合がある。
例えば、✔️ は以下のように 2 つの文字で表現される。
| コードポイント | 文字 | Unicode 名 | 一般カテゴリ |
|---|---|---|---|
U+2714 |
✔ |
HEAVY CHECK MARK | Other Symbol |
U+FE0F |
N/A | VARIATION SELECTOR-16 | Nonspacing Mark |
ここで、U+2714 (✔) は Other Symbol なので無視される。他方、これに付随する異体字セレクタ U+FE0F は Nonspacing Mark であり、アンカー ID に利用される。したがって、次のような見出しを作成した場合、異体字セレクタのみを記述する必要がある。
## ✔️
↓
[異体字セレクタのみのアンカー ID](#%EF%B8%8F)
%EF%B8%8F は VARIATION SELECTOR-16 の UTF-8 encoding 表示である。
この手の絵文字は、絵文字内の Emoji_Presentation=No ([[:emoji:]-[:Emoji_Presentation=Yes:]]) を調べれば良い。すると、219 のコードポイントが確認される⧉。これらの文字に異体字セレクタを付けて明示的に絵文字を Presentation にしている場合、同様の問題が生じるようになる。
当然ながら、絵文字内の Emoji_Presentation=Yes に U+FE0E (VARIATION SELECTOR-15) が付けられている場合もある。この場合でも異体字セレクタは %EF%B8%8E として表記する必要がある。
そのため、以下のように、異体字セレクタで見出しが異なる文字列に見なされる。
`U+FE0E` が付いた絵文字
## ℹ︎
↓
[VS-15 付き](#ℹ%EF%B8%8E)
----------
`U+FE0F` が付いた絵文字
## ℹ️
↓
[VS-16 付き](#ℹ%EF%B8%8F)
----------
異体字セレクタが付いていない絵文字
## ℹ
↓
[異体字セレクタなし](#ℹ)
参考
UnicodeData.txt のフォーマット(折りたたみ)
UnicodeData.txt のフォーマットを確認しておきたい。
UnicodeData.txt の各文字の情報は次のように書かれている。(; は区切り文字)
CodeValue;CharacterName;GeneralCategory;CanonicalCombiningClasses;BidirectionalCategory;CharacterDecompositionMapping;DecimalDigitValue;DigitValue;NumericValue;Mirrored;Unicode1.0Name;10646CommentField;UppercaseMapping;LowercaseMapping;TitlecaseMapping
例えば、ラテン文字大文字の A に関しては、次のように記述されている。
0041;LATIN CAPITAL LETTER A;Lu;0;L;;;;;N;;;;0061;
これはすなわち、以下のように読み取ることが出来る。
| UnicodeData | A |
|
|---|---|---|
| 0 | 符号値 (CodeValue) |
U+0041 |
| 1 | 文字名 (CharacterName) |
LATIN CAPITAL LETTER A |
| 2 | 一般カテゴリ (GeneralCategory) |
Lu (Uppercase Letter) |
| 3 | 正準結合クラス (CanonicalCombiningClasses) |
0 |
| 4 | 双方向性カテゴリ (BidirectionalCategory) |
L |
| 5 | 分解マッピング (CharacterDecompositionMapping) |
- |
| 6 | 数値 (DecimalDigitValue) |
- |
| 7 | 数値 (DigitValue) |
- |
| 8 | 数値 (NumericValue) |
- |
| 9 | 鏡像反転するか (Mirrored) |
N (No) |
| 10 | Unicode 1.0 のときの文字名 (Unicode1.0Name) |
- |
| 11 | コメント (10646CommentField) |
- |
| 12 | 対応する Uppercase (UppercaseMapping) |
- |
| 13 | 対応する Lowercase (LowercaseMapping) |
U+0061 |
| 14 | 対応する Titlecase (TitlecaseMapping) |
- |
逆に、ラテン文字小文字 a では、UppercaseMapping に 0041 が記載されている。
今回は、大文字から小文字を考えるため、
- 2 番目の
GeneralCategoryがLu - 13 番目の
LowercaseMappingに文字の割り当てがある
文字に関して注目する必要がある。軽く正規表現で調べてみると、1360 文字が該当した。(;Lu;.+?;[0-9A-Z]{4,5};[0-9A-Z]{0,5}$)
余談
誰に需要のある記事なのだろうか。
しかしながら、明文化されないと分からない部分だと思われる。特に、Qiita のアンカー ID における Slug 化はいくつかの記事で紹介されているが、具体的でありながらどれも不十分なように感じた。アンカー ID に利用できる文字は \p{Word} であることをハッキリしておきたい。
追記
- 2022/12/26: 異体字セレクタについて追記。