2013年4月25日

UnMHT Ver.6.3.0 のリリーススケジュール

UnMHT Ver.6.3.0 のリリース予定日が 2013年05月07日 に決定しましたー、ワーパチパチ!さっき一人で5分くらい考えて決めただけじゃん!イェイ!

[リリーススケジュール]

という事でいつもよりちょっと長めの翻訳期間 (どのくらい取ったらいいのかよく分かんない……) を取って、今日 BabelZilla に UnMHT Ver.6.3.0b3 をアップしました。新しい文字列はなんと3つだけ! unmht.properties の方はプロパティの名前を階層的に変更したから diff 取るとヒドいけど中身は何にも変わってないのでした。

[翻訳に関する情報]

今回は新機能がほとんど無いのでマニュアルは2カ所くらいしか変わってナイ。まぁ定期的に大きな区切りを持ってくるってのは開発してる感(?)があっていいんじゃないかしら。

[ユーザーズマニュアル (Ver.6.3.0 Beta)]

予定通りいけば、今回こそ Firefox のリリースとビッタリ被ってアワワワ、なんて事も回避できるハズ。

2013年4月17日

toString と valueOf を自分で定義する

JavaScript の Object は toString メソッドを定義すると暗黙的な文字列変換を制御できたのだー。

……実は JavaScript 1.0 の仕様だったんですね。仕様読め私。

という事でメモメモ。

let a2 = {
  x: 10,
  y: 20,
};
print (a2);

これだと結果は

[object Object]

こうなる。いつもの挙動。
でもこれじゃあ中身が何だかサッパリわからない。
そこで toString メソッドを定義する。

let a = {
  x: 10,
  y: 20,
  toString: function () {
    return "(" + this.x + "," + this.y + ")";
  }
};
print (a);

すると

(10,20)

こうなる。わぁ、なんてベンリ。

ちなみに JavaScript 1.1 の仕様に valueOf ってのもある。文字列変換だけじゃなくて、プリミティブな値が欲しい時に呼ばれるメソッド。

let a = {
  x: 10,
  y: 20,
  toString: function () {
    return "(" + this.x + "," + this.y + ")";
  },
  valueOf: function () {
    return "[" + this.x + "," + this.y + "]";
  }
};
print (a);
print (a + "");

1 個目の print では直接文字列表記を取得するので toString、2 個目の print ではその前に文字列演算があって、プリミティブな値を必要とされるので valueOf が呼ばれる。

(10,20)
[10,20]

たとえ文字列演算でも toString ではなく、valueOf が呼ばれる。

計算可能な値をラップするのに使えたりするかもしれない。でも不安なのでたぶん値を取り出すメソッドが getter を定義すると思う。

toString しか用意してなければプリミティブな値を必要とする場合にも toString が呼ばれる。

let a = {
  x: 10,
  y: 20,
  toString: function () {
    return "(" + this.x + "," + this.y + ")";
  }
};
print (a);
print (a + "");

すると

(10,20)
(10,20)

こんなかんじ。

実用にはどうかなーって気もするけど、デバッグにはいいかもしれない。

2013年4月14日

xpcshell で chrome:// な URI を使う

ようやくユニットテストを作り始めた私です。

さて、UnMHT のコードの中には chrome://unmht/... な URI を使える事を前提としたコードがいくつかあります。例えば StringBundle のあたりとか。
で、これをコマンドラインからテストしたい、となった時に例のごとく xpcshell を使うワケですが、当然アドオンとして動いてないので chrome://unmht/... みたいに書いても例外投げられて終わっちゃう。

chrome のプロトコルハンドラを上書きするとか色々考えたけど、どうにもうまく行かなくて困っていたのですが、ふと bootstrapped extension のコードの中で chrome.manifest を動的に登録する部分があったように思って調べたワケです。

// dir は chrome.manifest があるディレクトリ
Components.manager.addBootstrappedManifestLocation (dir);

ユニットテストは tests ディレクトリの中にあるので、テスト用の chrome.manifest には以下のように最低限のモノだけ書いておきます。

content unmht     ../chrome/content/
locale  unmht en-US ../chrome/locale/en-US/

これで chrome://unmht/... みたいに書いて普通に使えるようになります。

ちなみに resource:// な URI はアドオンと同じ手順で登録できます。

// uri はベースになるディレクトリの URI
Services.io.getProtocolHandler ("resource")
  .QueryInterface (Ci.nsIResProtocolHandler)
  .setSubstitution ("unmht", uri);

っていうか chrome.manifest でパスを上に辿れるなんて思わなかった……

2013年4月11日

JavaScript と正規表現の速度

UnMHT の中では主に MHT ファイルや、内部の HTML ファイルのパースの部分で文字列処理を多用するのですが、ファイルが大きくなってくるとこの処理の速度がけっこう体感として重要になってきます。

今のコードの基礎部分はけっこう昔に作ったもので、その頃は Firefox の JavaScript の実行速度はそんなに早いものではなく、JavaScript でループ回したり複数回比較するよりも正規表現を使って match、replace、split した方が速いという場合があったのですが、ハテ今は事情違うんじゃないの?ってコトで再調査です。

var fstream
= Cc ["@mozilla.org/network/file-input-stream;1"]
  .createInstance (Ci.nsIFileInputStream);

fstream.init (file, -1, 0, 0);

var bstream
= Cc ["@mozilla.org/binaryinputstream;1"]
  .createInstance (Ci.nsIBinaryInputStream);
bstream.setInputStream (fstream);

var data = bstream.readBytes (file.fileSize);

bstream.close ();

var boundary = "----=_Part_73251CB_12DB0E1.1365674809317";

var calcTime = function (name, f, count) {
  var startTime = new Date ().getTime ();
  
  for (var i = 0; i < count; i ++) {
    f ();
  }
  
  var endTime = new Date ().getTime ();
  
  show (name + ": " + (endTime - startTime) + " [ms]\n");
};

var parseBoundaryS = function () {
  var parts = [];
  
  var midBoundary = "\r\n--" + boundary + "\r\n";
  var midBoundaryLen = midBoundary.length;
  
  var endBoundary = "\r\n--" + boundary + "--\r\n";
  var endBoundaryLen = endBoundary.length;
  
  var end = 0;
  
  for (;;) {
    var start = data.indexOf (midBoundary, end);
    if (start == -1) {
      start = data.indexOf (endBoundary, end);
      if (start == -1) {
        parts.push (data.substr (end));
      }
      else {
        parts.push (data.substr (end, start - end));
      }
      break;
    }
    
    parts.push (data.substr (end, start - end));
    end = start + midBoundaryLen;
  }
  
  return parts;
};

var parseBoundaryR = function () {
  var escaped = boundary
  .replace (/([\\\[\]\(\)\^\,\.\{\}\|\?\!\-\*\+\$])/g, "\\$1");
  var rexp = new RegExp ("\r\n--" + escaped + "(?:--)?(?:\r\n)?");
  
  return data.split (rexp);
};

calcTime ("parseBoundaryS", parseBoundaryS, 100);
calcTime ("parseBoundaryR", parseBoundaryR, 100);

parseBoundaryS は単純な文字列処理 (indexOf) によって、データを boundary で分割するもの、parseBoundaryR は正規表現 (split) によって、データを boundary で分割するものです。実際に出来上がってくるデータは多少違いますが、気にせずいきましょう。ちなみに今の UnMHT には parseBoundaryR 相当のコードが使われています。

これを Firefox 21 で実行してみます。するとどうでしょう、

parseBoundaryS: 83 [ms]
parseBoundaryR: 299 [ms]

………全然遅いじゃん! 

こりゃ以降正規表現に頼りすぎるのも考えものかも。
もちろん本格的に書き換えるならボトルネックの調査が先でしょうけど。

ちなみに回数を 100 回にして古いマシンの Firefox 2 でテストした結果 (このために let じゃなくて var で書いた)

parseBoundaryS: 520 [ms]
parseBoundaryR: 1564 [ms]

え、やっぱり遅くね?

私は何の幻覚を見たのかな……それとももっと昔のバージョンだったのかな
確かに当時はかなり色々比較した上で正規表現版使おうって決めたハズだったんだけど、自信無くなってきたゾ

2013年4月10日

ウェブページで使用されているフォントを調べる

ウェブフォントが使われているページが結構増えているようです。日本語圏ではサイズの問題からあんまり実用的ではないのですが、アルファベットの少ない言語では結構便利そうなモノです。

さて、ウェブページを保存するとなると、使われているウェブフォントも保存しなくちゃいけません。しかし今のところ CSS を直接パースするという手段は取っていないので、要素を全部眺めながら使われているフォントを調べて行く必要があります。

そこで使うのが inIDOMUtils です。getUsedFontFaces メソッドに調べたい要素の入った Range を渡すと nsIDOMFontFaceList を返してくれます。

let domUtil = Cc ["@mozilla.org/inspector/dom-utils;1"].getService (Ci.inIDOMUtils);
let range = document.createRange ();
range.selectNode (node);
let fontList = domUtil.getUsedFontFaces (range);
for (let j = 0, length = fontList.length; j < length; j ++) {
  let font = fontList.item (j);
  dump (font.URI);
}

さて、node は調べたい要素なのですが、ここに document.body を入れればその中で使われているフォントを全部調べてくれます……と思って使ってました。実際調べてくれはするんです。

しかし、ページが複雑になってくる等、特定の条件で何故かフリーズするんです。詳細は調べ尽くせなかったので原因は他にあるかもしれませんが。

で、今のところ使っている回避策としてはテキストノード (nsIDOMText) のみに使用するという事。フォントが使われている場所、といえばほぼテキストノードなので、これで用件はだいたい満たせた状態になります。UnMHT では保存時に他のスタイル情報を取得するために DOM ツリーを端から端まで走査するので、そのついでに、という感じであればそんなに処理を追加する事なく書けたのはラッキーだったのかもしれません。

アイコンを動的に生成する

XUL ではいろんな場所に画像が表示できるようになっています。例えばリストボックスのセルであったり、メニューの項目の隣であったり。そういうアイコンが固定のものであれば画像を用意しておいてその URL を書くだけで良いのですが、画像を動的に変化させたい場合どういう手段があるでしょうか。

画像は開発中の字幕表示ソフトの設定画面です。字幕の色や種類に応じたアイコンを表示しています。テキストなら T、HTML で書くなら H、画像なら I、また文字色と枠の色と背景色も反映させます。

で、この画像、data URI で表現された SVG なのです。

SVG が登場した頃は色々触って遊んでいたのですが、個人がウェブ上に置くという点から言えば特に大きなメリットもなくそのまま忘れておりました。(Inkscapeのファイルフォーマットとしては多用していましたが……)

しかしこういう場合、SVG には大きなメリットがあります。画像の属性を直接記述できるし、一旦 canvas を通して色々するなんて面倒な事も必要ありません。今回はほぼ文字ですが、ちゃんとデータを用意すれば細かい画像にも使えそうです。
 

UnMHT ver.6.3.0b1

バグも落ち着いてきたので ver.6.3.0 をベータに移行しました。今回はあんまり大きな機能追加はナシで、コマゴマした修正ばっかり。 

リストの扱いは、開発中の別アドオンから引っ張ってきたもの。行のドラッグアンドドロップって案外標準ではサポートされてないのね。もちろん裏にあるデータとの整合性とか考えると(実装してても)結構面倒なモノだけど。

  • [新機能] ファイル名中の文字の置換のリストをドラッグアンドドロップで並べ替えできるようにしました
  • [新機能] ファイル名中の文字の置換のリストで項目を複製できるようにしました
  • [新機能] Thunderbird で [ページ中にヘッダーフィールドを表示する] オプションを追加しました
  • [新機能] SeaMonkey で MHT ファイルの情報をページ情報ダイアログに表示するようにしました
  • [新機能] 設定ウィンドウで最後に選択されていた下層のタブを記憶するようにしました
  • [新機能] ダイアログが開かれた時にボタンにフォーカスするようにしました
  • [新機能] MHT ファイルに favicon を保存するようにしました
  • [変更] JSON 形式の設定のエクスポートファイルフォーマットに変更しました (古い形式のファイルもインポートできます)
  • [変更] [メールの元のフィールドを格納する] で UnMHT が使用するフィールド名を持つフィールドは X-UnMHT-Original- プレフィックス付きで保存するようにしました
  • [変更] Thunderbird で [URL をコメントとして挿入] を無効にしました
  • [変更] nsIFilePicker.show の代わりに nsIFilePicker.open を使用するようにしました
  • [変更] XUL オーバーレイを簡略化しました
  • [修正] いくつかの addEventListener/removeEventListener で正しくない useCapture が使用されているバグを修正しました
  • [修正] MHT ファイルを開いている状態で UnMHT を無効にしてもイベントリスナが即座に削除されないバグを修正しました
  • [変更] Firefox 16 のサポートを終了しました
  • [変更] Thunderbird 16 のサポートを終了しました
  • [変更] SeaMonkey 2.13 のサポートを終了しました

ウェブページを保存するという事

UnMHT は開発当初は (名前が示す通り) MHT を表示するためのアドオンだったんだけど、途中でリクエストから保存機能を付ける事になったわけです。

で、ウェブページを保存する、といっても今の時代多くのページは動的なコンテンツを含んでるので、「どう」保存するかというのが結果に影響するのです。

UnMHT では現在3つの方法を提供しています
  1. 現在の状態
    HTML ファイルについては保存するタイミングの DOM ツリーから再構築する
  2. 現在の状態 (スクリプト削除)
    「現在の状態」からスクリプト関係の要素、属性だけ削除する
  3. 元のファイル
    HTML ファイルについてはサーバが返したものを使う
ウェブページが JavaScript を一切使用していなければどの方法でも結果は同じです。1、2と3の間で HTML のソースに多少の差はありますが、見た目には影響しません。

ところが JavaScript が使われていると事情が変わります。大きく影響するのは以下のようなもの。


ページの主要なコンテンツが JavaScript で生成される


「元のファイル」での保存には致命的です。早い話が保存したい内容がファイルに入っていないワケですから、「ウェブページを保存した」 とは言えない状態です。

じゃあ「現在の状態」なら良いかというと実はそうでもなくて、主要なコンテンツが2つ表示されるか、保存された内容が MHT ファイルを開いた途端消し飛んで新しい内容 (もしくは空白)に置き換わるか、という事が発生したりします。

これは最近何度かフォーラムで報告された画像表示の JavaScript の問題も含みます。画像をクリックするとページが暗くなって真ん中に拡大画像が表示される、というスタイルは結構見かけるようになりました。画像を表示する際に背景を生成する場合、この背景に相当する要素が重複して作成され、id も重複したために機能が正しく動作しないという事が起きると、見るも無惨にページの上半分が真っ黒になったりするのです。

また、生成されたものが DOM に直接反映されるものでなければ「現在の状態」でも保存できません。具体例でいうと Canvas の内容です。Canvas の内容は HTML の記述とは独立して存在しているので、その状態を保存した上で続きから動き始めるという事ができません。

UnMHT では、例外的な挙動として「現在の状態 (スクリプト削除)」の場合のみ Canvas の内容を data URI にして background-image として表示しています。これは JavaScript がこれ以降動かないために可能な回避策であり、「現在の状態」では使えない方法です。

結局この場合、ウェブページを意図通りに保存できるのは「現在の状態 (スクリプト削除)」だけです。

ページのどうでもいいコンテンツが JavaScript で生成される

「現在の状態」での保存の場合に不具合が出ます。これは主に広告を動的に差し込むという状況なので、早い話が広告が沢山出るのです。 2つに増える事もあれば、広告の差し込み方によっては3つ4つに増えます。保存したい情報は保存されているので「ウェブページを保存する」という希望は叶えられますが見づらくなります。

「元のファイル」の場合には広告は改めて1つだけ差し込まれる事になりますし、「現在の状態 (スクリプト削除)」であれば既に差し込まれていた広告が1つ表示されるだけです。

本来の URL でない場合にページが遷移する

「ウェブページを保存したい」のは「ユーザ」ですが、「ウェブページを保存してほしい」のは誰でしょう?

そんな人はほとんど居ないんじゃないでしょうか。オンラインマニュアルみたいに、古い情報を参照され続ける事が困るとか、理由は色々あると思います。いづれにせよ、ウェブサイトは保存する事を前提には作られていないのです。

その最たる例がコレです。URL をチェックして、オリジナルでないならオリジナルのページに遷移する。つまりオリジナル以外の場所に保存されたページは閲覧不可というワケです。

ウェブページの制作者の意図に忠実に従うなら、「そのようなウェブページは保存できない」というのが答えでしょう。

偶然他の用途で開発していた「 現在の状態 (スクリプト削除)」がユーザ側の希望を叶える結果にはなりましたが、これについては本当に正しかったのか未だに疑問ではあります。

ページの主要なコンテンツが JavaScript で動く

万能に思える「現在の状態 (スクリプト削除)」ですが、この場合は困りモノです。

例えばメニューが JavaScript で動くようなページだと、ページ遷移ができなくなります。もちろんそこが保存する目的でなければ問題はありませんけど。

また、オーディオやビデオを含むページで、その再生制御に JavaScript が使用されているとしたら、JavaScript を削除してしまうと再生できなくなってしまいます。こっちは重大な問題です。

「ウェブページを保存する」というのが「今見えている状態を保存する」ではなく「今動いている状態を保存する」となると、スクリプトを削除してはいけないのです。

ページの内容がブラウザによって異なる

他とはやや違った問題ではあるのですが、互換性への影響です。

これは何も JavaScript に限った話ではなく、サーバサイドで生成されるもの、クライアントに返されるものが違うという場合にも発生する問題です。UnMHT は Firefox 用のアドオンですから、当然保存する際のページの内容は Firefox 用になっているわけです。HTML や CSS のサポートの違いからくるハックであるとか、ブラウザによって見せる内容を変えたいであるとか、色々な理由からページの内容や挙動をブラウザごとに分けているウェブサイトも少なくはないでしょう。

そうなると、UnMHT で保存した MHT ファイルは、本来構造的には IE でも Opera でも開けるはずなのに、ページの内容が適切ではないために Firefox でしか見れない MHT ファイルになってしまうという事が起こります。

JavaScript で分岐をしている場合、「元のファイル」で保存する事で多少回避できるかもしれません。ただし、分岐した結果追加で読み込むファイルがブラウザごとに違う場合、不完全な保存状態という事になってしまいます。

サーバサイドで分岐している場合、もうこれは手のうちようがありません。Firefox 専用の MHT ファイルの出来上がりです。

----

と、いうように、「ウェブページを保存する」といっても何のために保存するか、どういうページであるかによって、適切な保存方法が変わってくるのですが、はたしてユーザはこの違いを一目で認識し、適切な保存方法を選択できるでしょうか。

おそらくそれはムリでしょう。作ってる私だってムリです。一旦保存して、開いてみて、不具合が出たら原因を考える、という手順を踏まないと適切な保存方法は選択できません。 一番無難なのが「現在の状態 (スクリプト削除)」だ、というくらいです。

また、そもそも3つから選択させるという事自体が混乱をうむような気もします。今の名前付け「現在の状態」「元のファイル」等が分かりやすいかというとそうでもないわけですし。

勢いで付けた保存機能ですけど、本格的に考えてみると奥が深いものですね。

アドオンのメモリリークチェック

UnMHT が bootstrapped extension になったのは開発する際にも色々メリットがあります。一番は何と言ってもそのものズバリ再起動しなくて良いという事。ソース弄る、ビルド、インストール、再起動、チェックという手順は結構面倒なもの。それがソース弄る、ビルド、インストール、チェック、になれば時間的にもかなり改善されます。

ところで、 bootstrapped になった事でやらなくてはならない事も増えます。それは正確な終了処理。それまではアドオンの寿命は Firefox の寿命と同じだったワケで、終了処理は別に書かなくても良かったワケですが、bootstrapped になると寿命が全く異なります。Firefox の起動中に、有効になり、無効になり、インストール、アップデート、アンインストールがそれぞれ複数回起きます。

そうすると問題になるのが終了処理。つまり無効にするタイミングや、アップデート、アンインストールのタイミングで Firefox の弄った部分を全て元に戻さなくてはいけません。

目に見える部分については分かりやすいけど、目に見えない部分は本当に元に戻ってるか、どこかに参照が残っていないかしっかりチェックしないといけません。

そういうとき便利なのが about:compartments とか about:memory?verbose とか。軽い用途なら about:compartments で事足ります。

アドオンを無効にした後で、このリストに自分のアドオンの URL が入っていればアウト、入っていなければ(多分)セーフという感じです。about:memory?verbose の方は他にも色んな情報が見れますが、無効にした結果 about:compartments から URL が消える状況でもしばらく残ってたりするので、単純なチェックにはちょっと向かないかもしれません。

xpcshell と jsctypes

普段のコマゴマとしたスクリプトのもう半分は JavaScript で書いています。最近だと ServerSide JavaScript が広まってきたようで、いろんな処理系がありますね。

私は xpcshell を使っています。なんたってアドオンと同じように書けるんだもん。というのも、xpcshell は XPConnect を使うための JavaScript シェルなのです。(xpcshell はバイナリとして配布はされてないので、mozilla のソースコードから自分でビルドする必要があります)

xpschell では XPCOM が使えるので、アドオンでできるような事はほぼ出来ます。ただしスクリプトを書く上で色々足りない機能もあったりします。

たとえば、popen が無いのです。他のコマンドと連携するようなコマンドラインアプリケーションを書くとなるとこれはイタイ。 system も XPCOM で無理矢理作れない事もないけど、やっぱり不便です。

xpcshell を使い始めた頃は xpcshell 本体のコードを書き換えてビルドしてました。 しかし、バージョンアップする度にパッチを当て直してビルド、というのも面倒だし、他所に持って行ったら使えないし、という事で困ってました。

そこに登場したのが jsctypes です。これを使うと外部のライブラリに定義されている C 等の関数が直接呼べます、ヤッタネ。

system については非常に簡単です。関数を定義するだけで動きます。

let Cu = Components.utils;

Cu.import ("resource://gre/modules/ctypes.jsm");

let lib_c = ctypes.open ("/usr/lib/libc.dylib");
let system
 = lib_c.declare ("system",
                  ctypes.default_abi,
                  ctypes.int,
                  ctypes.char.ptr);
system ("ls")
lib_c.close ();

popen はどうかというと、こっちはちょっと面倒だったりします。せっかくなので入出力両方できる popen が欲しいところですし、欲張って作ってみましょう。

let Ci = Components.interfaces;
let Cc = Components.classes;
let Cu = Components.utils;

let toUTF8 = function (text) {
  let converter
  = Cc ["@mozilla.org/intl/scriptableunicodeconverter"]
  .createInstance (Ci.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";
  return converter.ConvertFromUnicode (text) + converter.Finish ();
};

let toCString = function (s) {
  let u = toUTF8 (s);
  let len = u.length;
  let p = new (ctypes.char.array (len + 1));
  for (let i = 0; i < len; i ++) {
    p [i] = u.charCodeAt (i);
  }
  p [len] = 0;
  return p;
};

let lib_c = ctypes.open ("/usr/lib/libc.dylib");

let f_pipe
= lib_c.declare ("pipe",
                 ctypes.default_abi,
                 ctypes.int,
                 ctypes.int.array (3));

let f_fork
= lib_c.declare ("fork",
                 ctypes.default_abi,
                 ctypes.int);

let f_read
= lib_c.declare ("read",
                 ctypes.default_abi,
                 ctypes.int,
                 ctypes.int,
                 ctypes.char.ptr,
                 ctypes.int);
let f_write
= lib_c.declare ("write",
                 ctypes.default_abi,
                 ctypes.int,
                 ctypes.int,
                 ctypes.char.ptr,
                 ctypes.int);
let f_close
= lib_c.declare ("close",
                 ctypes.default_abi,
                 ctypes.int,
                 ctypes.int);
let f_dup2
= lib_c.declare ("dup2",
                 ctypes.default_abi,
                 ctypes.int,
                 ctypes.int,
                 ctypes.int);
let f_execv
= lib_c.declare ("execv",
                 ctypes.default_abi,
                 ctypes.int,
                 ctypes.char.ptr,
                 ctypes.char.ptr.array (4));
let f_exit
= lib_c.declare ("exit",
                 ctypes.default_abi,
                 ctypes.int,
                 ctypes.int);
let f_waitpid
= lib_c.declare ("waitpid",
                 ctypes.default_abi,
                 ctypes.int,
                 ctypes.int,
                 ctypes.voidptr_t,
                 ctypes.int);

let popen = function (cmd) {
  let pfd_pc = new (ctypes.int.array (3));
  let pfd_cp = new (ctypes.int.array (3));
  
  f_pipe (pfd_pc);
  f_pipe (pfd_cp);
  
  let pid = f_fork ();
  if (pid == 0) {
    f_close (pfd_pc [1]);
    f_close (pfd_cp [0]);
    
    f_close (0);
    f_dup2 (pfd_pc [0], 0);
    f_close (pfd_pc [0]);
    
    f_close (1);
    f_dup2 (pfd_cp [1], 1);
    f_close (pfd_cp [1]);
    
    let argv = new (ctypes.char.ptr.array (4));
    argv [0] = toCString ("/bin/sh");
    argv [1] = toCString ("-c");
    argv [2] = toCString (cmd);
    argv [3] = null;
    
    f_execv ("/bin/sh", argv);
    
    f_exit (1);
  }
  
  f_close (pfd_pc [0]);
  f_close (pfd_cp [1]);
  
  let p = {
    pid: pid,
    ifd: pfd_cp [0],
    ofd: pfd_pc [1],
    
    readline: function () {
      if (this.ifd == 0) {
        return null;
      }
      
      let iseof = true;
      let p = new (ctypes.char.array (1));
      let c = 0;
      let buf = "";
      do {
        let size = f_read (this.ifd, p, 1);
        if (size == 0) {
          break;
        }
        c = p [0];
        iseof = false;
        if (c == 13) {
        }
        else if (c == 10) {
          break;
        }
        else {
          buf += String.fromCharCode (c);
        }
      } while (c);
      
      return iseof ? null : buf;
    },
    write: function (str) {
      if (this.ofd == 0) {
        return null;
      }
      let p = toCString (str);
      return f_write (this.ofd, p, p.length - 1);
    },
    iclose: function () {
      f_close (this.ifd);
      this.ifd = 0;
    },
    oclose: function () {
      f_close (this.ofd);
      this.ofd = 0;
    },
    wait: function () {
      f_waitpid (pid, null, 0);
    }
  };
  
  return p;
};

本当なら返り値としては nsIInputStream と nsIOutputStream を実装したようなヤツがいいんでしょうけど、そこは面倒なので使いそうな適当なメソッドだけ用意します。(readline の実装はあんまりかな……)

C の関数とやりとりする時に重要なのが、データ型の変換。基本的な型なら jsctypes がやってくれますが、配列とかポインタとかになってくると自前でやる必要があります (toCString とか)。

userContent.css で無理矢理スクリプトを走らせる

特定のページでスクリプトを動作させる方法には色んな方法があります。アドオンを作るとか、GreaseMonkey のスクリプトを作るとか。

それを userContent.css だけでやっちゃおうというオハナシ。

まず、userContent.css にスクリプトは書けません。だって CSS ですから。じゃあどうするのかというと、XBL を使います。

例として、 unmht.org の「あ」を落下させてみましょう。

userContent.css には以下のようにして、「あ」に対して -moz-binding を指定します。

@-moz-document domain(www.unmht.org) {
img[src="/parts/a.png"] {
  -moz-binding: url("userContent.xml#fall");
}
}

さてスクリプトの実体はというと、userContent.xml というファイルに書きます。別にこのファイル名である必要はなくて、-moz-binding でそう書いたから、というだけです。userContent.xml の後の #fall は fall という id の binding を使うという事で、名前を変えれば1つのファイルの中に複数の動作を書けます。

<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl">
  <binding id="fall">
    <implementation>
      <constructor>
<![CDATA[
var target = this;
var y = 0, vy = 0;
var update = function () {
  y += vy;
  vy += 1;
  target.style.top = y + "px";
  if (y < 1000) {
    setTimeout (update, 100);
  }
};
update ();
]]>
      </constructor>
    </implementation>
  </binding>
</bindings>

スクリプトの中では、スタイルを指定した要素は this でアクセスできます。

この2つのファイルを作って Firefox を再起動させてから、 www.unmht.org を開くと「あ」が落下します。userContent.css の反映には Firefox の再起動が必要なのでちょっと面倒ですね。xml の方は書き換えれば即座に反映されます。

スタイルからスクリプトを走らせる事のメリットとしては、動的なサイトにも簡単に対応できるという事です。ページロード時に走るタイプのスクリプトでは、その後にページ内容が追加された場合に対応するのがタイヘンなのですが、スタイルであれば要素が追加されたら即座にスタイルが解釈され、結果スクリプトが実行されます。

注意点としては、-moz-binding は重複指定できないという事。つまり自分が使うとなると他の場所での指定が無効になっちゃうので、場合によってはデフォルト動作に問題が起きるかも。

て事で、本格的な用途では使えないかもしれない豆知識でした。

古いバージョンは捨てていこう

もうだいぶ前の話になるけど、UnMHT ver.6.0.0 から Firefox 16 未満のサポートが終了しました。それまでは互換性は可能な限り保つように、例えば Mac OS 9 の Wazilla でも動くように、という事を考えてたんだけど、色んな理由があってポリシーを変更。
  1. 互換用のコードが増えてくると保守性が落ちる
  2. テストが大変(むしろテストできてなかった)
  3. 新しいものが使えない!
昔は(今も?)場当たり的なバグ修正をしてたので、互換用の分岐が一体何のためなのか(むしろ分岐が互換用なのかどうかすら)コメントを書いてなかったのです(書けよ!)。で、それが増えてくると新機能の追加時に分岐を見て頭をかかえるワケ。

テストに関しては、実際 AMO の中の人から「動かねえじゃねえか!」って怒られちゃった経緯もあって。古いモノ用の部分を壊さずに開発してたつもりでも、結構壊しちゃってるよね。テストを全バージョンでやるのはタイヘンだし、次第にテストがおざなりに……。 ユニットテストとか自動テストとか導入したほうがいいんだろうねぇ。
 それと、新しい構文や関数が使えないのは結構イタイ。新しいコンポーネントとか、コンポーネントの新しい機能とかなら、機能の存在をチェックできるし、「古いバージョンでは使えません」 で通すんだけど、新しい構文を使うとなると古いバージョンで一切動かなくなっちゃう。let とか map とか使いたいしー。実際 let とか map、filter って開発の効率に大きく影響するモノだと思う。もちろんそれまではナシで開発してきたんだから、下がるって事はないんだけど、目の前に便利そうなモノが並べられてるのに使えない、ってなるとモチベーションが下がっちゃう。バグ回避に有効なものもあるし。


実際、新しい構文、関数を取り入れてからコードがとてもスッキリしたような気がしますよ(人に見せて「どう?」って聞いた事はないけど)。モジュール分けも進むし、意味と表記が近くなったというか、そんな感じ?

最近は JavaScript も変革が進んでるみたいで、色んな新しい機能が導入されてます。Firefox 22 では Arrow Function が導入予定だとか。早く使いたいなー。とは言っても、さすがに最新バージョンしかサポートしないようではアンマリなので、使えるようになるのは Firefox 25 が出る頃カナ。

高速リリースに関して否定的な意見もあるみたいだけど、新しいものを積極的に取り入れながら開発するには便利なものだよ。だって古いバージョンがどんどん切れるんだもん。

ブログ開設

UnMHT 開発の事、プログラミングとかの気になった話、絵や音楽、その他全然関係ない話なんかを書くブログです。