日本語の文章にルビ(ふりがな)を振る記法として、いちばん普及しているのが青空文庫の記法です。

<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> は入れておく
  • 閉じ忘れは目視で見つからない。機械で数える
  • 字数カウント用に、記法を除去する関数を別に持つ

実装そのものは五十行ほどで終わります。手間がかかるのは、そのあとの検査のほうでした。


この文庫の縦書きリーダーの実装については 縦書きリーダーをブラウザで作る に書いています。実際にルビの入った作品は 作品一覧 から読めます。