米印がエスケープ文字として使われてきたが、米印は文字としてもあるので処理が混乱する原因になる。
米印から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()` する」ことです。**