KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

AozoraEpub3ConverterTest.javaを米印からESCAPE_MARKER方式に書き換え

米印がエスケープ文字として使われてきたが、米印は文字としてもあるので処理が混乱する原因になる。

米印からESCAPE_MARKER方式にテスト内容も書き換える必要がある。

         String str;
            str = converter.convertGaijiChuki("|※[#縦線]縦線※[#縦線]《※[#縦線]たてせん※[#縦線]》", true, true);
            System.out.println(str);
            Assert.assertEquals("|※|縦線※|《※|たてせん※|》",str);

今までは、内部処理でエスケープするときに※をつけていたがを使っている。 public static final char ESCAPE_MARKER = '\u0001';

予想:|※|縦線※|《※|たてせん※|》 実際:||縦線|《|たてせん|》

ChatGPT:

このテスト結果なら、原因は convertRubyText() の先頭で を通常文字として扱っていることです。

入力は、

|※|縦線《たてせん》※|

期待値は、

<ruby>|縦線<rt>たてせん</rt></ruby>|

つまり、

※|

という並びの は、内部エスケープ用の前置マーカーとして消費する必要があります

一方、以前の

あ※※《1

では、

※※

の2個の は通常の「米印」なので残し、

`ESCAPE_MARKER《`

だけを内部エスケープとして処理します。

したがって、ここで必要なのは を単純に削除することではなく、※| のペアを内部エスケープとして認識する処理です。

convertRubyText() の冒頭をこうする

現在の

if (ch[i] ==CharUtils.ESCAPE_MARKER) {
    if (i+1<end) {
        convertReplacedChar(buf, ch, ++i, noTcy);
    }
    continue;
}

に加えて、 の内部エスケープも処理します。

ただし、今回の入力はまだ ESCAPE_MARKER 化されていません。

|※|縦線《たてせん》※|

なので、

if (ch[i] =='※'&&i+1<end&& (ch[i+1] =='|'||ch[i+1] =='《'||ch[i+1] =='》'||ch[i+1] =='#')) {
    i++;
    convertReplacedChar(buf, ch, i, noTcy);
    continue;
}

とすれば、今回の

※|

を、

として出力できます。


ただし、今回の修正ではこちらの方が重要です

元々 convertGaijiChuki() が、

※[#縦線]

※|

に変換しているなら、 を「内部エスケープのマーカー」として使う方式と、米印そのものの が衝突します。

例えば、

あ※※[#米印]※[#始め二重山括弧]1

などでは、 の個数だけで判定すると今回の元バグに戻ります。

そのため、最終的には次の形に統一するのが安全です。

convertGaijiChuki()
    ↓
※[#縦線]
    ↓
※|                 ← 青空文庫変換結果

内部エスケープ変換
    ↓
ESCAPE_MARKER|     ← 解析用

convertRubyText()
    ↓
|

つまり convertRubyText() に「※|ならエスケープ」と持ち込むより、convertGaijiChuki() の後に ※特殊文字ESCAPE_MARKER特殊文字 へ変換する段階を作る方がよいです。


今回のテストを通すだけなら、最小修正はこれです。

if (ch[i] ==CharUtils.ESCAPE_MARKER) {
    if (i+1<end) {
        convertReplacedChar(buf, ch, ++i, noTcy);
    }
    continue;
}

if (ch[i] =='※'&&i+1<end) {
    switch (ch[i+1]) {
    case'|':
    case'《':
    case'》':
    case'#':
        // 「※+特殊文字」は内部エスケープとして扱うi++;
        convertReplacedChar(buf, ch, i, noTcy);
        continue;
    default:
        break;
    }
}

これで、

|※|縦線《たてせん》※|

は、

<ruby>|縦線<rt>たてせん</rt></ruby>|

になります。

ただし、この修正は暫定策です。今回の一連の問題の本命は「 をエスケープ文字として使うこと自体をやめて ESCAPE_MARKER に分離する」ことなので、最終版では convertGaijiChuki()convertRubyText() の間で ※| 等を ESCAPE_MARKER| に変換するのがよいです。

You:

buf = converter.convertRubyText("|\u0001|縦線《たてせん》\u0001|");
System.out.println(buf);
Assert.assertEquals( "<ruby>|縦線<rt>たてせん</rt></ruby>|",buf.toString());

ChatGPT:

はい。このテストに変更したことで、設計がかなり明確になりました。

buf=converter.convertRubyText(
    "|\u0001|縦線《たてせん》\u0001|"
);
System.out.println(buf);

Assert.assertEquals(
    "<ruby>|縦線<rt>たてせん</rt></ruby>|",
    buf.toString()
);

この場合、convertRubyText()ESCAPE_MARKER 自体を出力してはいけません

入力は内部的に、

| ESCAPE_MARKER | 縦線 《 たてせん 》 ESCAPE_MARKER |

です。

期待する処理は、

| ESCAPE_MARKER | 縦線 《 たてせん 》 ESCAPE_MARKER |
  ↓
|            | 縦線 《 たてせん 》            |

つまり、

  • ESCAPE_MARKER| を通常文字として出力

  • その はルビ開始文字として扱わない

  • 《たてせん》 は通常のルビ

  • ESCAPE_MARKER|

  • ESCAPE_MARKER は出力しない

です。

convertRubyText() の先頭

したがって、先ほどのコードはこれで正しいです。

if (ch[i] ==CharUtils.ESCAPE_MARKER) {
    // 内部エスケープ文字自身は出力しない。// 次の1文字をリテラルとして出力する。if (i+1<end) {
        convertReplacedChar(buf, ch, ++i, noTcy);
    }
    continue;
}

特に、

buf.append(ch[i]);

絶対に入れないでください。


さらに case '|'isInternalEscapedChar() は不要

現在のような、

case'|':
    if (!CharUtils.isInternalEscapedChar(ch, i)) {
        ...
    }
    break;

は、内部エスケープ処理をループ冒頭で消費するなら、

case'|':
    if (rubyStart!=-1) {
        convertTcyText(buf, ch, rubyStart, i, noTcy);
    }
    rubyStart=i+1;
    noTcyPre=noTcy;
    inRuby=true;
    break;

でよいです。

なぜなら、

ESCAPE_MARKER|

の場合は ESCAPE_MARKER の処理で まで消費して continue するため、case '|' に到達しないからです。

同様に、

case'《':

も、

case'《':
    inRuby=true;
    rubyTopStart=i;
    break;

で構いません。


このテストで確認すべき結果

入力:

|\u0001|縦線《たてせん》\u0001|

文字列としては、

| ESCAPE_MARKER | 縦線 《 たてせん 》 ESCAPE_MARKER |

です。

ループは概ねこう動きます。

|                  → rubyStart = 0
ESCAPE_MARKER       → 次の|を消費して出力
|                  → ここではルビ判定しない
縦線                → rubyStart の対象
《                  → rubyTopStart
たてせん             → ルビ
》                  → ruby生成
ESCAPE_MARKER       → 次の|を消費して出力

結果:

HTML

|縦線たてせん

になります。

---

### 重要な点

この方式にすると、元の問題だった

あ※※《1

も、

あ※※ ESCAPE_MARKER 《 1

として、

あ※※《1

になります。

つまり今回の修正方針では、

**`※` の個数・パリティでエスケープ判定する必要がなくなります。**

したがって最終的には `convertRubyText()` 内の

CharUtils.isInternalEscapedChar(ch, i)

による判定も不要になり、`ESCAPE_MARKER` を見つけた時点で\*\*「次の1文字を無条件にリテラルとして消費する」\*\*という単純な仕様にできます。

You:

org.junit.ComparisonFailure: expected:<\[<ruby>\]|縦線<rt>たてせん</rt></ru...> but was:<\[|<ruby>\]|縦線<rt>たてせん</rt></ru...>

ChatGPT: 

この結果なら、原因は `ESCAPE_MARKER` の処理位置です。

現在の入力は、

"|\u0001|縦線《たてせん》\u0001|"

で、期待値は、

HTML

<ruby>|縦線<rt>たてせん</rt></ruby>|

ですが、実際は、

HTML

|縦線たてせん

になっています。

つまり最初の

| ESCAPE_MARKER |

で、最初の `|` を**ルビ開始として確定してしまった後**、`ESCAPE_MARKER` を処理しています。

---

## 問題はここ

現在の処理順はおそらく、

if (ch[i] ==CharUtils.ESCAPE_MARKER) { ... continue; }

ですが、`i == 0` の `|` の時点で、

rubyStart=i+1; inRuby=true;

となっています。

その後の `ESCAPE_MARKER` で、

convertReplacedChar(...)

を実行しているため、

ESCAPE_MARKER|縦線...

という状態になっています。

---

# 今回の内部表現では `ESCAPE_MARKER|` は「ルビ本文の一部」

ここが重要です。

入力:

|\u0001|縦線《たてせん》

は、

| ESCAPE_MARKER | 縦線 《 たてせん 》

です。

青空文庫としての意味は、

||縦線《たてせん》

なので、最初の `|` と `ESCAPE_MARKER|` の `|` を合わせて、

||縦線《たてせん》

として扱う必要があります。

したがって `ESCAPE_MARKER` を見つけたら、単純に `buf` に出力するだけではなく、**現在の `rubyStart` を維持したまま、次の文字だけを本文側に含める**必要があります。

---

# 一番簡単な修正

`ESCAPE_MARKER` の処理で `convertReplacedChar()` を呼ぶのではなく、**何もしないで次の文字だけスキップする**方法があります。

つまり、

if (ch[i] ==CharUtils.ESCAPE_MARKER) { i++; continue; }

です。

ただしこれだけだと `|` も出力されません。

そこで、

if (ch[i] ==CharUtils.ESCAPE_MARKER) { if (i+1<end) { // ESCAPE_MARKER は出力しない// 次の文字は通常文字として処理するi++; } continue; }

ではなく、**ESCAPE\_MARKER の直後の文字を通常のループに戻す**必要があります。

---

# おすすめは `escaped` フラグ方式

例えばループの外に、

booleanescaped=false;

を置きます。

そしてループ冒頭:

for (inti=begin; i<end; i++) {

if (ch[i] ==CharUtils.ESCAPE_MARKER) {
    escaped=true;
    continue;
}

if (escaped) {
    // この1文字はリテラルとして扱うescaped=false;

    // ルビ開始・終了判定を完全にスキップして、// 通常文字として処理するconvertReplacedChar(buf, ch, i, noTcy);
    continue;
}

// 通常処理
これなら、

ESCAPE_MARKER|

の `|` は、

convertReplacedChar()

だけで処理され、

case'|':

には入りません。

---

## ただし今回のテストではもう一つ問題があります

入力:

|\u0001|縦線《たてせん》\u0001|

で、最初の `|` は**ルビ開始記号**です。

したがって、

i=0 |

rubyStart = 1 inRuby = true

となるのは正しいです。

その後、

i=1 ESCAPE_MARKER i=2 |

では、`|` をルビ開始記号として再解釈してはいけません。

この方式なら、

if (escaped) { escaped=false; convertReplacedChar(buf, ch, i, noTcy); continue; }

で `case '|'` を飛ばせるので、

rubyStart = 1

を維持できます。

最終的に、

rubyStart = 1 rubyTopStart = 5

となり、

|縦線《たてせん》

から、

HTML

<ruby>|縦線<rt>たてせん</rt></ruby>

を生成できます。


したがって先頭部分はこうしてください

booleanescaped=false;

for (inti=begin; i<end; i++) {

    // 内部エスケープ開始if (ch[i] ==CharUtils.ESCAPE_MARKER) {
        escaped=true;
        continue;
    }

    // 内部エスケープされた1文字if (escaped) {
        escaped=false;

        // ESCAPE_MARKER 自体は出力しない。// 次の1文字をリテラルとして出力する。convertReplacedChar(buf, ch, i, noTcy);
        continue;
    }

    // 以下、従来の処理

そして、この方式なら既存の

switch (ch[i]) {
case'|':
    if (!CharUtils.isInternalEscapedChar(ch, i)) {
        ...
    }

CharUtils.isInternalEscapedChar()

は不要です。

switch (ch[i]) {
case'|':
    if (rubyStart!=-1) {
        convertTcyText(buf, ch, rubyStart, i, noTcy);
    }
    rubyStart=i+1;
    noTcyPre=noTcy;
    inRuby=true;
    break;

case'《':
    inRuby=true;
    rubyTopStart=i;
    break;
}

の判定も同様です。


これで今回の結果は

入力
| ESCAPE_MARKER | 縦線 《 たてせん 》 ESCAPE_MARKER |

| | 縦線 《 たてせん 》 |

HTML

|縦線たてせん

となります。

**ポイントは `ESCAPE_MARKER` の直後の文字を `continue` で通常ループから追い出すのではなく、「ルビ記号としての判定だけをスキップして、通常文字として `convertReplacedChar()` する」ことです。**