HTMLDocument と Numeric character references

Java で HTML を読み込み、その内容を元に別の HTML を出力するちょっとしたプログラムを書こうとしています。こんな時、Java には HTMLEditorKit という便利なクラスがあります。これを使うと HTML を解析して HTMLDocument という HTML の内容を表すドキュメントクラスのインスタンスを生成してくれます。これがどんなに便利かは古い記事ですが、この記事等を読むとわかります。


さて逆に HTMLDocument の内容 HTML として出力しようとする時は HTMLWriter を使うことができます。ところがこれを使って日本語の HTMLDocument を出力しようとすると “&amp#12371;&amp#12398;&amp#12392;&amp#12371;” のような内容で出力されてしまいます。一瞬「また、文字化け??」と思ってしまいましたが、これはれっきとした HTML の書式で“Numeric character references (NCR)”と呼ばれるものだそうです。日本語では数値文字参照というようです。


「うーん、何で EUC や シフト JIS で出してくれないの??」と思い、Web 検索するもこのあたりの情報は乏しいです。仕方がないのでクラスのソースを見てみると、HTMLWriter クラスで思いっきり次のように書かれていました。


if (chars[counter] < ' ' || chars[counter] > 127) {
(中略)
// If the character is outside of ascii, write the
// numeric value.
output(“&#”);
output(String.valueOf((int)chars[counter]));
output(“;”);
}
(JDK 5.0 HTMLWriter クラスの “protected void output(char[], int, int)” のソース抜粋)


ascii でなけりゃ問答無用で NCR だということです。作った人はそれで済む国の人なのでしょう。日本でエディタ等を使って HTML を書くような人はシフト JIS や EUC で出力されないと困りますよね。


今回は EUC で出力したいのでどうしたものかと考えました。 HTMLWriter のサブクラスを作って該当の output 関数のみ override して HTMLWriter.output の丸コピーのチョイ直しという手も考えました。しかしそうすると HTMLWriter クラスの replaceEntities という重要なフラグが private なので参照することができません。本来 HTMLWriter が NCR で出力するかネイティブな文字コードで出力するか動作を選択できるようになっているべきなのでしょう。


そこで今回は使い捨てのプログラムだからと割り切り、出力する Writer の方にサブクラスをつくって”&#数字;” が来たらいつも使っているダブルバイトの文字に直すようにすることにしました。


で書いてみたのが、下のクラスです。このクラスのインスタンスを使って HTMLWriter を生成します。先に参照したソースの部分の output 呼び出し3連発に依存する書き方です。使い捨てなのでいいのです。


public class NCR2NativeWriter extends java.io.OutputStreamWriter {
private int count;
private String tempString; // NCR format string
public NCR2NativeWriter(OutputStream out, String s)
throws java.io.UnsupportedEncodingException {
super(out, s);
count = 0;
}
public void write(char[] cbuf, int off, int len)
throws java.io.IOException {
if (off != 0) {
super  .write  (cbuf, off, len);
return;
}
if (count == 0 && len == 2 && cbuf[0] == ‘&’ && cbuf[1] == ‘#’) {
count = 1;
return;
} else if (count == 1) {
int i = 0;
for (; i < len; i++) {
if (cbuf[i] > ‘9’ || cbuf[i] < '0') {
break;
}
}
if (i >= len) {
tempString = new String(cbuf, 0, len);
count = 2;
return;
} else {
super  .write  (“&#”);
}
} else if (count == 2) {
if (len == 1 && cbuf[0] == ‘;’) {
char c[] = new char[1];
c[0] = (char)new Integer(tempString).intValue();

super  .write  (c, 0, 1);
count = 0;
return;
} else {
super  .write  (“&#”);
super  .write  (tempString);
}
}
super  .write  (cbuf, 0, len);
count = 0;
}
}


Java のまとまったプログラムってほとんど書いたことないのでアレですが、いつか誰か(含自分)の参考になればということで。