文字列と UTF-8 バイト列の相互変換2006年10月23日 23時11分

やっていることは「高度な JavaScript 技集」の「UTF-8 <-> UTF16 変換」と同じ。

function toUTF8Octets(string) {
  return unescape(encodeURIComponent(string));
}

function fromUTF8Octets(octets) {
  return decodeURIComponent(escape(octets));
}

encodeURIComponentencodeURI でもいい (むしろそのほうが処理する文字種が減って速くなりそう) が、decodeURIComponentdecodeURI にすると一部の文字 ("?"、"#" など) がデコードされなくなる。

使いどころ

Base64 エンコードする関数 (「高度な JavaScript 技集」の base64encode や Firefox など Gecko 系ブラウザで実装されている btoa) では渡された文字列をバイト列として扱う (btoa では \u0100 以上の文字を含む文字列を渡すとエラーになる)。なので、あらかじめ文字列をバイト列に変換しておく必要がある。

btoa(toUTF8Octets("Base64 エンコード"));
// QmFzZTY0IOOCqOODs+OCs+ODvOODiQ==

なお、data URI を扱う際に Base64 は必須ではない。英数字以外の文字が多く含まれている場合 (画像など) は Base64 エンコードしたほうが長さの点から有利だが、そうでなければ内容を URL エンコードするだけで十分だ。

location.href = "data:text/plain;charset=utf-8," + encodeURI("日本語も OK");

余談だが、C 言語の atoi が頭にあると、つい atob が文字列を Base64 エンコードするものと思ってしまうが (b = Base64 という連想)、実際はその逆だ。btoa は Binary to ASCII ということなのだろうか。

XUL アプリケーションでの使いどころ

通常、テキストファイルに書き込むときは nsIOutputStream::write を使うが、これは文字列をバイト列として扱うので、日本語文字などをそのまま渡すと文字化けする。テキストファイルを読み込むときも、nsIScriptableInputStream::read で返されるのはバイト列を表現する文字列である。

そのため、ファイル入出力の際に日本語文字などを扱うためには、Gecko 1.8 / Firefox 1.5 から導入された nsIConverterInputStream / nsIConverterOutputStream、または nsIScriptableUnicodeConverter を使うのだが、いちいちほかの XPCOM コンポーネントを利用するまでもない・文字コードは UTF-8 決め打ちでいいというときは以下のようにできる。

// file に content を UTF-8 で書き込む
function writeTo(/* nsIFile */ file, /* String */ content) {
  var stream = Components.classes['@mozilla.org/network/file-output-stream;1']
                         .createInstance(Components.interfaces.nsIFileOutputStream);
  stream.init(file, 0x02 | 0x08 | 0x20, 420, -1);
  content = toUTF8Octets(content);
  stream.write(content, content.length);
  stream.close();
}

また、prefs.js から設定を読み書きする際も、nsIPrefBranch::setComplexValue / getComplexValuensISupportsString の組み合わせを使わずとも、日本語文字などを扱える。

var pref = Components.classes["@mozilla.org/preferences;1"]
                     .getService(Components.interfaces.nsIPrefBranch);

// 設定の保存
pref.setCharPref(name, toUTF8Octets(value));

// 設定の読み込み
var value = fromUTF8Octets(pref.getCharPref(name));

さて、このほかにも文字列・バイト列を扱う XPCOM コンポーネントは数多く存在するが、それぞれの場面で文字列とバイト列のどちらを使えばいいかは、XULPlanet の XPCOM Reference または使用するインターフェースの IDL ファイルから確認できる。引数、返り値、または属性の型が AStringwstringPRUnichar* なら文字列、ACStringstringchar* ならバイト列だ。

スクリプト中の文字列リテラルに関しては、HTML / XUL の script 要素から読み込まれた場合は文字列として扱われるが、JavaScript で作成された XPCOM コンポーネントや mozIJSSubScriptLoader から読み込まれたスクリプトではバイト列として扱われる。どちらの場合でも同じく扱いたいときは以下のようにすることもできる。(そもそもそのような文字列は locale に分離すべきだが。)

// スクリプトの文字コードが UTF-8 であることが前提
if ("あ".length == 1)
  function L(string) { return string; }
else
  function L(string) { return decodeURIComponent(escape(string)); }

var s = L("日本語文字");
s.length; // 5

それにしても、nsIScriptableUnicodeConverter::ConvertFromUnicode / ConvertToUnicode メソッドの名前が大文字から始まっているのは気持ち悪い。なぜ誰も指摘しなかったんだ。

参考

注意点

escape / unescape は、ECMAScript では非規範的な仕様とされ、JavaScript では非推奨なので、気になる人は以下のようにするといいかもしれない。

function toUTF8Octets(string) {
  return encodeURI(string).replace(/%(..)/g, function (s, code) {
    return String.fromCharCode("0x" + code);
  });
}

function fromUTF8Octets(octets) {
  return decodeURIComponent(octets.replace(/[%\x80-\xFF]/g, function (c) {
    return "%" + c.charCodeAt(0).toString(16);
  }));
}

Safari の Date の修正2006年10月25日 20時57分

Safari の Date#setMonth にバグがあると聞いて試してみたところ、そもそも Date コンストラクタからして月の負数指定に対応していない様子。やれやれ仕方のない子だな WebKit はというわけで Date オブジェクト自体を書き換えるスクリプトを書いてみた。適用すると Date コンストラクタ、 Date.UTCDate#setMonthDate#setUTCMonthDate#setFullYearDate#setUTCFullYear で月を指定する際に負数を使えるようになる。

13 以上の数も扱えないことに気づいたのでテストケースを修正。スクリプトはそのままで通用する。

書いている途中で気づいたのだが、WebKit、というより JavaScriptCore では関数の引数と arguments オブジェクトのプロパティが値を共有しない。つまり、function f(arg1, arg2, ...) { ... } としたとき、argN を変更しても arguments[N-1] にはその変更が反映されないし、逆も同じだ。まったくもってこれはひどい。Date#setMonth への負数指定も、引数と arguments オブジェクトでの値の共有も、すでに開発版では修正されているようだが、よくまあここまで気づかなかった / 放っておいたものだなと思ってしまう。

それから、これは Konqueror / KJS でも同じだが、組み込みオブジェクトのプロトタイプオブジェクト (String.prototype など) には constructor プロパティを設定できない。仕方がないので上述のスクリプトではインスタンスに constructor プロパティを設定している。

ほかにも開発版ではいろいろと修正されているようだったので、まとめて Safari の JavaScript の不備を大幅に更新した。DOM 周りの拡張にもいくらか触れている。

なお、Date#setMonth を直すだけでいいのなら、以下のようにするのが簡単かと思う。

Date.prototype.setMonth = function (month, date) {
  return this.setFullYear(
    this.getFullYear() + Math.floor(month / 12),
    ((month %= 12) < 0) ? month + 12 : month,
    (arguments.length >= 2) ? date : this.getDate()
  );
};

Firefox 2 でフィードの XSLT を有効にする2006年10月28日 18時17分

各地で話題になっている、Firefox 2 が RSS フィードの製作者スタイルシートを無視する問題だが、「あの不定記 Firefox 2 のフィードプレビューで XSLT が無視されてしまう」にてフィードプレビュー自体を無効にする方法が載っている。しかし、そもそもスタイルシートが結び付けられてないフィードも多いわけで、そういったフィードにはフィードプレビューを使いたいところ。

そこで、上記記事をヒントに、XML がフィードかどうかを判別する XPCOM コンポーネントを置き換えてやればいいのではと思い立って、xml-stylesheet 処理命令が含まれる場合はフィードとみなさないコンポーネントを JavaScript で書いてみたのだが、C++ で書かれたコンポーネントを JavaScript で書いたコンポーネントで置き換える方法がわからずに挫折。

仕方なく処理の流れを追っていたら、フィードを解析する時点でスタイルシートの有無もチェックしていることが判明。だが、その情報がどこでも活用されていないようだったので、あれこれと見て回った結果、Firefox インストールディレクトリの components ディレクトリにある FeedConverter.js の 188 行目、if (result.doc)if (result.doc && !result.stylesheet) に書き換えてやれば、製作者スタイルシートを持つフィードにはそれを適用し、そうでないものにはフィードプレビューを使うことができるようになることがわかった。diff もとったので置いておく。

--- components/FeedConverter.original.js 2006-10-26 04:47:08.000000000 +0900
+++ components/FeedConverter.js 2006-10-28 17:01:30.902990400 +0900
@@ -185,7 +185,8 @@
            Cc["@mozilla.org/network/io-service;1"].
            getService(Ci.nsIIOService);
       var chromeChannel;
-      if (result.doc) {
+      // If a stylesheet is associated with the feed, use it.
+      if (result.doc && !result.stylesheet) {
         // If there was no automatic handler, or this was a podcast,
         // photostream or some other kind of application, we must always
         // show the preview page...

例によって改造は自己責任で。この先、差分アップデートがあったときにどうなるかとかはまったくわからないのでそのつもりでよろしく。

書き忘れていたが、フィードプレビューは、フィードの MIME タイプが applicatoin/atom+xml でも application/rss+xml でもなく、かつ文字コードが ASCII と互換性のないものであるときは有効にならない。つまり、Firefox 2 でフィードを見ている人全員に、フィードの製作者スタイルシートが適用されるようにするするためには、フィードの MIME タイプを application/xml、文字コードを UTF-16 にするという手もある。

Firefox 2 のフィードの判別方法は IE 7 のフィードの判別方法と同じであり、リソースの先頭 512 バイトしか見ない。なので、Firefox 2 および IE 7 でフィードに製作者スタイルシートを適用させるためには、フィードの MIME タイプを application/xml にし、先頭 512 バイトを空白文字またはコメントなどで埋めるという手もある。

なるほど。実体参照を使うという手があったか。まあこれは文字参照でも同じだと思うので、以下のようにするのがお手軽かもしれない。(Firefox 2 でのみ確認、一般のフィードリーダで読めるかは不明。)

<rdf:RDF xmlns:rdf="&#104;ttp://www.w3.org/1999/02/22-rdf-syntax-ns#"
         xmlns:rss="&#104;ttp://purl.org/rss/1.0/"