たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
機能を追加した時にen-USに追加したエンティティを他のロケールに追加し忘れて「○○語の環境で設定画面が動かねえぞゴラァ!!!」という風なバグ報告を貰うことがあまりに多いので(そして自分で気づけないので)、そういうミスを事前に防ぐためのスクリプトを書いてみた。
find_missing_entries_from_locales.js
コンテキストメニュー拡張などの、任意のスクリプトをXPConnect特権付きで実行できるツールでこのスクリプトを実行すると、以下のように動作する。はず。
var file = Cc['@mozilla.org/file/local;1']
.createInstance(Ci.nsILocalFile);
file.initWithPath('C:\\temp\\target.jpg');
var fileStream = Cc['@mozilla.org/network/file-input-stream;1']
.createInstance(Ci.nsIFileInputStream);
fileStream.init(file, 1, 0, false);
var binaryStream = Cc['@mozilla.org/binaryinputstream;1']
.createInstance(Ci.nsIBinaryInputStream);
binaryStream.setInputStream(fileStream);
var bytes = binaryStream.readBytes(fileStream.available());
binaryStream.close();
fileStream.close();
var base64 = btoa(bytes);
Bug 364586 - nsXmlRpcCLient.js conversion to base64 is slowで「JavaScriptでBase64エンコードするの遅いから組み込みの関数使えやゴラァ」と提出されていたパッチを見て知った。バイナリファイルの内容をバイト列の配列として読んでゴニョゴニョしなくても、単純に、バイナリのインプットストリームからreadBytes()
でバイト列を文字列として取得してbtoa()
にかければOK、だそうだ。試してみたら確かにちゃんといけた。
The Burning Edge見てたら、こんなバグがFIXEDになっていた。
text/htmlなHTMLドキュメントをXMLとして扱うにあたって、HTML5の仕様に合わせる形になるという事のようだ。namespaceURI
がnull
から"http://www.w3.org/1999/xhtml"
へ、localName
がすべて大文字からすべて小文字へ、それぞれ変わる、と。
以前の挙動は以前の挙動で古い仕様には合致していたはずなので、時代の移り変わりをしみじみと感じる。
FireMobileSimulatorでのローカルプロキシ実装の試みを見て、UxUでデバッグ用ローカルプロキシみたいな事をできるようにしてみたい、と思った。
ただ、HTTPのことはこれっぽっちも分からない。ソケット通信も、一応独自プロトコルっぽいものを使って別プロファイルで動作中のFirefoxからテスト結果を受け取るということはできるようになったけど、それ以上の事は分かってないまま。なので、まじめにローカルプロキシを立てる以外の方法で、「特定のURIにアクセスしようとした時だけ、あらかじめ定義しておいたルールに従って別のリソースを返す」ということをできるようにしてみようと考えた。
僕が現在把握している方法としては、以下の物がある。
http-on-modify-request
イベントのタイミングでリダイレクトするやり方。shouldLoad()
の中でリダイレクトするやり方。1はthorikawaさんが頑張っておいでなので、それに期待している(ソースがGPL互換ならUxUにそのまま入れられるので)。他人のフンドシ。他力本願。ここでは後の2つについての挫折の経緯だけ書き留めておく。
結論を先に言っておくと、2も3も実装上の制限により全滅っぽい。やはり、ローカルプロキシをちゃんと実装するしか完全な解決策はないようだ。thorikawaさん期待age。
http-on-modify-request
イベントを使うやり方これは、現在FireMobileSimulator等ですでに使われている。nsHttpChannelはリクエストを送信する前にnsIObserverServiceを通じてhttp-on-modify-request
イベントを、レスポンスが返ってきた後にhttp-on-examine-response
とかhttp-on-examine-merged-response
とかhttp-on-examine-cached-response
とかのイベントを通知する(SubjectはnsHttpChannel自身)ので、このタイミングでリダイレクトをしようという話。
FireMobileSimulatorの場合、nsHttpChannelの現在の通信をキャンセルした上で、nsHttpChannelからnsIWebNavigationを引っ張ってきてloadURI()
で新しく通信を始めてる。
var redirected = 'http://www.example.com/';
var observer = {
observe : function(aSubject, aTopic, aData) {
if (aTopic == 'http-on-modify-request') {
var httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel)
.QueryInterface(Ci.nsIRequest);
// ここでキャンセル
httpChannel.cancel(Components.results.NS_ERROR_FAILURE);
// リクエストし直し
httpChannel.notificationCallbacks
.getInterface(Ci.nsIWebNavigation)
.loadURI(redirected, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
}
};
Cc['@mozilla.org/observer-service;1']
.getService(Ci.nsIObserverService)
.addObserver(observer, 'http-on-modify-request', false);
通常のbrowserやiframeの場合はこれでいいんだけど、img要素やXMLHttpRequestからの通信では失敗する。具体的にはhttpChannel.notificationCallbacks.getInterface(Ci.nsIWebNavigation)
の時点でエラーになる。例えばXMLHttpRequestによる通信なら、httpChannel.notificationCallbacks.getInterface(Ci.nsIXMLHttpRequest)
とすれば元のリクエストを取得できるので、こんな感じで場合に応じて再リクエストの方法を振り分けてやる必要がある。(XMLHttpRequestの場合については、どうやれば再リクエストできるのかまではたどり着けてない。もしかしたら無理なのかも。)
spec
を書き換える場合nsIChannelのURI
プロパティはnsIURIインターフェースの読み取り専用プロパティなので、通信先のURIを書き換えることはできない……ように見える。でも実はnsIURIのspec
プロパティの方は書き換え可能なので、ここに新しいURIを代入することででもリダイレクトできてしまう。
observe : function(aSubject, aTopic, aData) {
if (aTopic == 'http-on-modify-request') {
var httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
httpChannel.URI.spec = redirected;
}
とはいえこの方法は全くお勧めできない。動く場合もあるけど、動かない場合もある、という感じで実に不安定だ。nsHttpChannelの実装を見れば分かるけど、http-on-modify-request
が通知された段階ですでにその時のリクエスト先URIに基づいた初期化処理がいくつか終わってしまっているので、それと矛盾するURIを設定する(例えば、HTTPのリクエストだった物をFile URLにリダイレクトするとか)と、この後の内部処理でエラーが起こるっぽい。
http-on-examine-response
の方ではnsIHttpChannelのsetResponseHeader()
を利用できるので、Location
ヘッダにURIを設定してみたんだけど、ダメだった。残念ながらヘッダを解釈してくれなかった。
nsHttpChannelの実装を見たら、HTTPのステータスコードが301とか302とかの時だけLocation
ヘッダを見るようになってた。ステータスコードを保持しているプロパティは読み取り専用なので、強制的にLocation
ヘッダを読ませるということはできそうにない。
shouldLoad()
を使うやり方これはURNサポートなどで実際に使っている。詳しい方法は2007年当時のエントリに書いてあるけど、要約するとこういうことだ。
やってみると、一見上手く動いてくれてるように見えるんだけど、XPConnect特権がある実行コンテキストで作成したImageやnsIXMLHttpRequestのインスタンスからの通信が捕捉されなくて、UxUの用途では使えない感じだった。まあ仮に捕捉できたところで、元の通信を止めて別のURIで通信するようにさせる方法は分かってない(もしかしたら無いかもしれない)んだけど。
あと、shouldLoad()
の第2引数はnsIURIインターフェースなのでこれのspec
プロパティを書き換えたらどうだろう?と思ってやってみたけど、これもうまくいかなかった。browser要素等での読み込みの場合だと、nsDocShellのInternalLoad()
でエラーが発生する。実装を見た感じでは、nsIContentPolicyに処理が渡ってくるより前に元のURIに基づいてセキュリティ関係の機能が初期化されてるので、その後でURIを書き換えたのがいけなかったんじゃないかと思う。
とにかく、nsHttpChannelのインスタンスが作成された後からどうこうしようと思うのがそもそも手遅れくさい。それより前のステップでアクセス先のURIを書き換えようと思ったら、ローカルプロキシを立てる以外に手は無いようだ。
いいかげん諦めてMozilla Partyの発表資料作ることにします……
PHPとかerbのようなテンプレートをJavaScriptで。という話に書いたやつの続き。
大切な事なので3回言います。
<% for (var i = 0; i < 3; i++) { %>
今日は<%= today %>です。
<% } %>
オーケー?
こういう文字列をテンプレートとして解釈したい場面は多分よくあると思う。この例だと、today
という変数をどっか外部から与えて値を埋め込むことになる。で、この変数をどうやって渡したらええねん、と。
グローバル変数をがんがんに使って構わないのであれば、先にグローバル変数としてtoday
を定義しておけばいい。eval()
で実行されるコードの中からも普通に参照できる。
しかしアドオンのコードのように、個々のグローバル関数やグローバル変数の寿命が長い事が予想されるスクリプトでは、この手は使えない。
this.
を付けるparseTemplate()
の仕様としてはあくまで「書かれたコード片は第2引数で渡されたオブジェクトをthisとして実行されます」という風にしておいて、テンプレート風に書かれている方の文字列内のJavaScriptコード片では必ずthis.today
という風に書くようにする、という方法もある。
しかしいちいちthis.
を付けるのは面倒だし、そもそも僕がこのような記法を知ったきっかけであるERBでは、self.
なんて書く必要がなかった。同じような物は同じように使えた方がいい。なのでこの方法も使いたくない。
var
で宣言し直す先のエントリに当初書いていたコードでは、parseTemplate()
の第2引数で渡したオブジェクト(ハッシュ)のプロパティを走査してそれをvar
で変数として宣言し直す、ということをしてた。外の名前空間をなるべく汚さないための苦肉の策だ。
if (aContext && typeof aContext == 'object') {
for (var prop in aContext) {
if (!aContext.hasOwnProperty(prop)) continue;
__parseTemplate__codes.unshift('var '+prop+' = aContext.'+prop+';');
}
}
こんな感じ。
でも、これだとエラーになりそうな場合がけっこう考えられる。例えばハッシュのキーが01234
という文字列だと、実行されるJavaScriptのコードはvar 01234 = aContext.01234;
となってしまい、eval()
にかけた時点でSyntaxError: missing variable name
と怒られてしまう。
なので、こういうエラーの元になる名前のプロパティを除外して、安全な物だけをvar
で宣言する、ということをやりたかった。言い換えると、そのプロパティの名前を構成している文字列がJavaScriptの識別子として妥当かどうかを判別したかった。
ということでやっと、このエントリの本題に入る。
UxU 0.5.11に入れ損ねた。次版で標準のヘルパーメソッドに入れるけど、割といいかげんな実装で、たいした規模じゃないから、テストケースの中に直接書いて使ってもいいと思う。
function parseTemplate(aCode, aContext) {
var __parseTemplate__codes = [];
aCode.split('%>').forEach(function(aPart) {
var strPart, codePart;
[strPart, codePart] = aPart.split('<%');
__parseTemplate__codes.push('__parseTemplate__results.push('+
strPart.toSource()+
');');
if (!codePart) return;
if (codePart.charAt(0) == '=') {
__parseTemplate__codes.push('__parseTemplate__results.push(('+
codePart.substring(1)+
') || "");');
}
else {
__parseTemplate__codes.push(codePart);
}
});
var __parseTemplate__results = [];
with(aContext|| {}) {
eval('(function() { '+__parseTemplate__codes.join('\n')+' }).call(aContext|| {})');
}
return __parseTemplate__results.join('');
}
var source = <![CDATA[
大切な事なので3回言います。
<% for (var i = 0; i < 3; i++) { %>
今日は<%= today %>です。
<% } %>
オーケー?
]]>.toString();
var params = {
today : (new Date()).toString()
};
var result = parseTemplate(source, params);
レガシーだけどクロスブラウザな書き方だったら、こうか。
function parseTemplate(aCode, aContext) {
var __parseTemplate__codes = [];
aCode = aCode.split('%>');
var strPart, codePart;
for (var i in aCode) {
aCode[i] = aCode[i].split('<%');
strPart = aCode[i][0];
codePart = aCode[i].length == 1 ? null : aCode[i][1] ;
__parseTemplate__codes.push('__parseTemplate__results.push(unescape("'+
escape(strPart)+
'"));');
if (!codePart) continue;
if (codePart.charAt(0) == '=') {
__parseTemplate__codes.push('__parseTemplate__results.push(('+
codePart.substring(1)+
') || "");');
}
else {
__parseTemplate__codes.push(codePart);
}
}
var __parseTemplate__results = [];
with(aContext|| {}) {
eval('(function() { '+__parseTemplate__codes.join('\n')+' }).call(aContext|| {})');
}
return __parseTemplate__results.join('');
}
var source = '大切な事なので3回言います。\n'+
'<% for (var i = 0; i < 3; i++) { %>\n'+
' 今日は<%= today %>です。\n'+
'<% } %>\n'+
'オーケー?';
var params = {
today : (new Date()).toString()
};
var result = parseTemplate(source, params);
マルチプルタブハンドラについてRockridge氏ほかから「メニューをカスタマイズできるようにしてくれ」という要望が挙がっていたのだけれども、Menu Editorという素晴らしいアドオンがあるのに自前で同じような機能を再実装するのは徒労感しか無いなあと思ったので、開き直って、マルチプルタブハンドラの設定ダイアログを以下のようにしてみた。
Menu EditorのAPIをよく分かってないので(ていうかそもそも公開APIなのかどうかも知らない)、タブ選択時のメニューをカスタマイズできるようにするためにちょっと強引な方法を使ってる。
で、同じようなこと(他のアドオンの有無を調べた上で設定を開く)を何度も書きたくなかったので、設定ダイアログに加えた変更の要点を他のアドオンと連携しやすくするためのライブラリとして分離してみた。
if (window['piro.sakura.ne.jp'].extensions.isInstalled('my.extension.id@example.com') &&
window['piro.sakura.ne.jp'].extensions.isEnabled('my.extension.id@example.com'))
window['piro.sakura.ne.jp'].extensions.goToOptions('my.extension.id@example.com');
アドオンがインストールされているかどうか・有効か無効かを調べるだけならFUELで事足りるので、あまり使い出がないといえば使い出がない。まあThunderbird 2あたりでだったらニーズがあるかもだけど。
ちなみにFUELで書く場合、アドオンがインストールされているかどうかはApplication.extensions.has('my.extension.id@example.com')
、有効か無効かはApplication.extensions.get('my.extension.id@example.com').enabled
で分かる。設定ダイアログのChrome URLを調べるAPIはなくて、それでこのライブラリを作ることにした次第です。
以前書いたtabbrowser要素のswapBrowsersAndCloseOther()
メソッドの上書きの話で書いてたサンプルが使えなくなっていたので直した。というか自作アドオンの機能を使おうとして正しく動かなかったのでコードを改めて見てみて、メソッドの書き換えの対象になってる箇所に変更があった事に気付いた(4月16日に変更が行われていたようだ)。
以下は、Shiretoko 3.5b5preに合わせて修正した後のバージョンのサンプルコード。
if ('swapBrowsersAndCloseOther' in aTabBrowser) {
eval('aTabBrowser.swapBrowsersAndCloseOther = '+
aTabBrowser.swapBrowsersAndCloseOther.toSource().replace(
'{',
'$& MyAddonService.destroyTab(aOurTab);'
).replace(
'if (aOurTab == this.selectedTab) {this.updateCurrentBrowser(',
'MyAddonService.initTab(aOurTab); $&'
)
);
}
if (aOurTab == this.selectedTab)
を目印にして「タブを入れ換えた後の再初期化処理」を入れてたんだけど、同じif文がこれより前の位置に増えたせいで、コードの挿入位置がずれてしまい、本来期待していた順番とは異なる順番で処理が行われてしまっていた。if文の中まで目印に含めるようにして一応回避したけど、これはこれで、if文の中を書き換えるアドオンがあると破綻してしまう可能性があるわけで……何かいい方法はないものだろうか。
今回の場合に限らず、「メソッドの入れ替えではなく動的な書き換えを行う」やり方には、こういう変更に逐一追従しなくてはならないという弱点がある。メソッドのインターフェース(引数の数や返り値)はそうそう変わらなくても、中身は結構頻繁に変わる。なので、気をつけておかないといけない。
もう1つの弱点として、書き換え対象のメソッドの中で参照されている変数がそのメソッドが定義された変数スコープからしか参照できないものであった場合に、それへの対処も必要となる。その変数の値がグローバルな名前空間からアクセス可能なものであれば、メソッドの書き換え時にその変数を同時に宣言し直してやればいいんだけど……
(function() {
var attrName = 'foo';
window.ExampleAddonService = {
method : function(aNode) {
aNode.setAttribute(attrName, true);
}
};
})();
// メソッドの書き換えには成功するが、attrNameという変数が
// 見つからなくなってしまうので、実行時にエラーになる
eval(
'window.ExampleAddonService.method = '+
window.ExampleAddonService.method.toSource().replace(
'{',
'{ AnotherAddonService.preProcess(aNode);'
)
);
この例のような場合になると、もうお手上げ。
それでも動的なメソッドの書き換えの方を僕が積極的に使う最大の理由は、同じやり方でメソッドを書き換える他のアドオンとの互換性を維持するためだ。別名で元のメソッドを保持しておくやり方の場合、他のアドオンが元のメソッドの名前で関数オブジェクトを取得しても、その関数の内容は元のメソッドとは全然違うから、書き換えられなくて動かなくなる、という事態が起こり得る。それを避けるためには、上の例のようなお手上げな事例を除き、可能な限り元のメソッドを動的に書き換えた方が良いということになる。
Tree Style Tab 0.7.2009040901公開した。アニメーション効果の実装の他に、細かいバグ修正も色々。
あと、実際どんな感じかというのをわかりやすく示せるかなと思って、デモ動画も作ってみた。CamStudioもNiVEも使うの久しぶり(っていうかVistaにしてからは初)だから、やり方思い出すのに苦労したよ……
<object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/M9dUfyoHz3E&hl=ja&fs=1"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/M9dUfyoHz3E&hl=ja&fs=1" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="344"></embed></object>
ヌルヌル動いてるのは倍速再生してるわけではなく、これで等倍速です(一応)。タブの影はbox-shadowで描画してるので、Firefox 3.0.xだと影無しになります。
しかしYouTubeはアップロードが簡単になってるわ画質が上がってるわで、いつの間にかすんげーパワーアップしてますね。Stage6とかあった頃とは隔世の感があります。
以下余談。
実装の最後の段階になって困った点として、画面外にタブが開かれた時にそこまで自動スクロールしてくれないという問題が起こってた。スクロールしても、新しく開かれたタブの一個前のタブの位置にスクロールしちゃったりして。上の動画で言うと、0:57あたりで画面下の方でサブツリーを展開した時に、子タブが画面内におさまるように自動でスクロールしてるけど、これがちゃんと動かなくなってた。
なんでこんな問題が起こってたかというと、「そのタブが画面外にあるのかどうかを判定する」「そのタブの位置までスクロールする」といった処理が全部「タブの座標」を基準にしてたせいで、アニメーション開始時点やアニメーション中の中途半端な座標を元に処理を行ってしまい、もうシッチャカメッチャカになってた、という……
上記の処理を行う時に、アニメーション中の座標とアニメーション終了後の座標とのズレをきちんと考慮して計算してやればいいってだけの話なんだけど、普通に考えるとこれがめんどくさい。それぞれのタブがちょっとずつズレて表示されてるわけだから、座標を調べたいタブだけじゃなくてそれより前(上)にあるタブ全部について、そのタブはアニメーション中か?とか、そのタブは非表示か?とかを判別しながらオフセット値を調べないといけないわけで……いちいちそんな計算するのはめんどくさすぎる。メンテナンスし辛そうだし、コードの量が多くなりそうだし、真面目に書く気しないです(大学生の頃だったらやってたかもだけどね……時間は有り余ってたから)。
で、代わりに以下のようなやり方を思いついた。
コードにするとこんな感じ。
getYOffsetOfTab : function(aTab)
{
return document.evaluate(
'sum((self::* | preceding-sibling::*[not(@tab-collapsed="true")])'+
'/attribute::tab-y-offset)',
aTab,
null,
XPathResult.NUMBER_TYPE,
null
).numberValue;
},
sum()
は、渡されたノードセットの値を数値として合計したものを返すXPathの関数。受け取る結果の型をXPathResult.NUMBER_TYPE
と指定すれば、計算結果の数値を直接得られる。XPathはうまく使えばこんな風に、処理対象のノードの絞り込みだけじゃなくその後の処理(ここでは計算)まで一発で済ませられるので面白い。
トゥイーンの話を受けてごろたんがさらに発展的な話を書いてくれた。で、「ほうほう、こういうのをeasingと言うのか……」と自分の無知っぷり&何も知らんくせに偉そうなことを語る厚顔さに恥じ入りながら先のエントリを少し手直しした。
で、JSTweener という Tweener (as3 のモーショントィーンライブラリ) 互換のライブラリを使うと、標準で様々な easing 関数が利用できたり、タイマーが一つなので、数百, 数千オブジェクトをモーションさせるときにはだいぶ高速になる。
というのを読んで、確かに先のエントリに書いた物はいっこいっこのタブごとにタイマー走らせるから効率悪いよなあ、と思い、1個のタイマーだけで複数のアニメーションを走らせるための簡単なライブラリを作ってみた。ツリー型タブの開発版にも早速組み込んでる。
window['piro.sakura.ne.jp'].animationManager.addTask(
)
にアニメーション用の関数その他の引数を渡してやると、それを共通のタイマーで処理する、というごく単純なもの。window['piro.sakura.ne.jp'].animationManager.removeTask(
)
で関数の登録を解除してやる。クロージャ使って書くこと前提の不親切なAPIですんません……