日本語の文章にルビ(ふりがな)を振る記法として、いちばん普及しているのが青空文庫の記法です。
<ruby>漢字<rp>(</rp><rt>かんじ</rt><rp>)</rp></ruby>
<ruby>任意の語<rp>(</rp><rt>よみ</rt><rp>)</rp></ruby>
この文庫でも同じ記法を採用して、十八篇の作品を組んでいます。以下は、実際に実装して動かしてみて分かったことです。
記法のルール
青空文庫の記法には、ルビの振り方が二通りあります。
1. 自動ベース(漢字の直後に《》を置く)
<ruby>琥珀<rp>(</rp><rt>こはく</rt><rp>)</rp></ruby>の色
《》の直前にある漢字の連続が、自動的にルビの対象(ベース)になります。上の例なら「琥珀」の二文字にルビが乗ります。
ほとんどの場合はこれで足ります。
2. 明示ベース(全角の縦棒|で開始位置を指定)
<ruby>一文字ぼかし<rp>(</rp><rt>いちもんじぼかし</rt><rp>)</rp></ruby>
全角の縦棒 | から《》の直前までがベースになります。使うのは次の三つの場合です。
- ひらがな・カタカナにルビを振りたいとき(自動ベースは漢字しか拾わない)
- 漢字の一部だけに振りたいとき(「東京都」ではなく、都まで含めたい/含めたくない)
- 直前の漢字を巻き込みたくないとき(「大阪城」の直前に別の漢字が続いている場合など)
縦棒は半角の | ではなく全角の | です。ここは間違えやすいところです。
Markdown サイトへの実装
Markdown を HTML に変換するパイプライン(remark / rehype など)を使っている場合、Markdown のパーサに渡す前に、先にルビ記法だけを HTML へ置換するのがいちばん簡単です。
const KANJI = '\\u4E00-\\u9FFF\\u3005\\u3006\\u30F6';
// 1) 明示ベース: <ruby>base<rp>(</rp><rt>reading</rt><rp>)</rp></ruby>
s = s.replace(/|([^《》|\n]+)《([^《》\n]+)》/g, (_m, base, ruby) =>
`<ruby>${base}<rp>(</rp><rt>${ruby}</rt><rp>)</rp></ruby>`
);
// 2) 自動ベース: 直前の<ruby>漢字連続<rp>(</rp><rt>reading</rt><rp>)</rp></ruby>
const autoRe = new RegExp(`([${KANJI}]+)《([^《》\\n]+)》`, 'g');
s = s.replace(autoRe, (_m, base, ruby) =>
`<ruby>${base}<rp>(</rp><rt>${ruby}</rt><rp>)</rp></ruby>`
);
ポイントは三つあります。
明示ベースを先に処理する
順序が逆だと、<ruby>東京<rp>(</rp><rt>とうきょう</rt><rp>)</rp></ruby> の「東京」が自動ベースの正規表現に先に食われて、縦棒が本文に残ります。必ず明示ベースを先に置換します。
漢字の範囲に「々」「〆」「ヶ」を含める
上のコードの 々〆ヶ がそれです。含めないと、「人々」が「人」だけをベースにしてしまい、「人(ひとびと)々」という表示になります。
<rp> を入れる
<rp> は、ruby 要素に対応していない環境でルビを括弧で囲むための要素です。現代のブラウザはすべて ruby に対応していますが、テキスト抽出やコピー&ペースト、読み上げの挙動が環境によって変わるので、入れておいて損はありません。
傍点(圏点)も同じ仕組みで
青空文庫では傍点に [#「〜」に傍点] という注記を使いますが、この記法は書くのがつらいので、この文庫では独自に <em class="bouten">テキスト</em> としました。
s = s.replace(/\^\^([^\^\n]+)\^\^/g, (_m, t) => `<em class="bouten">${t}</em>`);
CSS 側は一行です。
.bouten {
font-style: normal;
-webkit-text-emphasis: filled dot var(--shu);
text-emphasis: filled dot var(--shu);
-webkit-text-emphasis-position: over right;
text-emphasis-position: over right;
}
text-emphasis-position: over right を指定しておくと、縦書きでは文字の右側、横書きでは上側に付きます。指定しなくてもだいたい正しく出ますが、WebKit 系で位置が変わることがあるので、書いておくほうが安全です。-webkit- 付きの記述もまだ要ります。
落とし穴
実際に十八篇を組んで、引っかかったのは以下でした。
閉じ忘れが表示上バレない
<ruby>漢字<rp>(</rp><rt>かんじ のように </rt><rp>)</rp></ruby> を忘れると、置換がマッチせず、記号がそのまま本文に出ます。ところが縦書きの本文をざっと読んでいると、これが意外と目に入りません。
対策は機械的な検査です。この文庫では、公開前に走らせるチェックスクリプトで 《 と 》 の数が一致しているかを数えています。同じスクリプトで ^^ の個数が偶数かどうかも見ています。目視は当てになりません。
同じ語に何度もルビが振られる
書いているうちに、同じ難読語の二度目・三度目にもルビを振ってしまいます。読者にとっては煩わしいだけなので、ルビは初出のみと決めておくといいです。
文字数カウントがルビで狂う
読了時間の表示や字数の管理をしている場合、ルビの文字をそのまま数えると実際より多く出ます。カウント用に、記法を除去してから数える関数を別に用意しておきます。
const stripped = markdown
.replace(/|([^《》|\n]+)《[^《》\n]+》/g, '$1') // 明示ベース → ベースだけ残す
.replace(/《[^《》\n]+》/g, '') // 自動ベース → ルビを捨てる
.replace(/\^\^([^\^\n]+)\^\^/g, '$1');
この文庫では最初、これを用意せずに字数を記録していたため、実際の約二倍の数字を管理表に書いていました。数字を扱うときは、測り方を先に決めるべきでした。
縦書きだと、ルビ以外のところで崩れる
ルビ自体は問題なく縦書きになりますが、同じ本文の中で以下は横倒しになります。
| 対象 | 正 | 誤 |
|---|---|---|
| 疑問符・感嘆符 | ? !(全角) | ? !(半角) |
| ダッシュ | ――(U+2015 を二つ) | —(U+2014) |
| 数字 | 漢数字 | 半角/全角アラビア数字 |
とくに半角の ? は縦書きで横に寝ます。ルビの実装が終わったら、このあたりも一緒に検査対象にしてしまうのが早いです。
まとめ
- 置換は明示ベース(
|)が先、自動ベース(漢字+《》)が後 - 漢字の文字クラスに
々〆ヶを含める <rp>は入れておく- 閉じ忘れは目視で見つからない。機械で数える
- 字数カウント用に、記法を除去する関数を別に持つ
実装そのものは五十行ほどで終わります。手間がかかるのは、そのあとの検査のほうでした。
この文庫の縦書きリーダーの実装については 縦書きリーダーをブラウザで作る に書いています。実際にルビの入った作品は 作品一覧 から読めます。