Now browsing the SCRAPBLOG weblog archives.
宿題
ScrapBookのページ保存機能ではCSSから参照される画像は無条件でダウンロードされる。たとえば、CSS中に
#hoge { background-image: url('foobar.png'); }
というルールがあれば、たとえ現在のページ中に id=”hoge” が存在しないとしても foobar.png はダウンロードされる。
この仕様だと確かに保存機能の精度は高くなるが、場合によっては無駄なファイルが大量にダウンロードされる。特に最近はCSSを利用したデザインが多い傾向にあるようで、不要な画像ファイルをダウンロードすることによるデータサイズの肥大がかなり気になってきた。
そこで、CSSからは参照されているが、実際のページ(HTML)からは参照されない画像をダウンロードしないための方法を考えてみようと思う。
ロジック:
- CSSのルールから参照される各画像について、それがHTMLから参照されているかどうか判別する
- 現在表示しているHTMLから「実際に」参照されている画像をリストアップし、CSSから参照されている画像のリストと突き合わせて不要ファイルを検出する
→現在表示しているHTMLから「実際に」参照されている画像のリストアップは「ページ情報」の「メディア」に相当するので、これが参考になるかも?
ユーザインタフェース:
- [設定]の[取り込み]タブに、試験的なオプションを追加する
→十分なデバッグの後、正式採用する - 編集ツールバーに[不要ファイル削除]機能を追加する(ボタンまたはメニュー)
→DOMイレーサーなどでページから削除された(=参照されなくなった)画像を物理的に削除する機能としても使える
autoCheck=”false” と autocheck=”false”
XULの button および toolbarbutton 要素に対して type=”checkbox” 属性を指定すると、オンオフ可能なチェックボックス型ボタンとなる。通常、ユーザがマウスまたはキー操作でボタンをクリックすると、自動的にチェック状態が変化してチェックマークが反転される。チェック状態の変化に伴って何か処理を行うのであればこれで問題ないが、何らかの条件付でチェック状態を変化させたいような状況もありうる。そのような場合は button および toolbarbutton 要素に対して autoCheck=”false” 属性を指定して、自動的にチェック状態変化させないようにすればよい。その代わり、スクリプトで checked プロパティを反転させ、チェック状態を変化させるのである。
<script type="application/x-javascript"><![CDATA[ function checkStateHandler(aEvent) { var target = aEvent.originalTarget; // 条件付きで checked プロパティを反転させる if ( (new Date()).getSeconds() % 2 == 0 ) { target.checked = !target.checked; alert("checked property = " + target.checked); } } ]]></script> <!--チェックボックス型ボタン--> <button type="checkbox" autoCheck="false" label="button" oncommand="checkStateHandler(event);" /> <!--チェックボックス型ツールバーボタン--> <toolbarbutton type="checkbox" autoCheck="false" label="toolbarbutton" oncommand="checkStateHandler(event);" />
一方、XULの menuitem 要素に対して type=”checkbox” 属性を指定すると、オンオフ可能なチェックボックス型メニューアイテムとなる。また、 type=”radio” を指定すると、兄弟関係にあるすべての menuitem からひとつだけ選択可能なラジオボタン型メニューアイテムとなる。これらについても同様に、条件付きでチェック状態を変化させたい場合はどうすればよいか。これらの要素で自動的にチェック状態を変化させるには、 autocheck=”false” 属性を指定してやればよい(先述した autoCheck と異なり、c が小文字であることに注意)。また、スクリプトから手動でチェック状態を切り替える際には、先述のように checked プロパティを反転させる代わりに、 checked 属性を反転させる。
<script type="application/x-javascript"><![CDATA[ function checkStateHandler2(aEvent) { var target = aEvent.originalTarget; // 条件付きで checked 属性を反転させる if ( (new Date()).getSeconds() % 2 == 0 ) { target.setAttribute("checked", !(target.getAttribute("checked") == "true")); alert("checked attribute = " + target.getAttribute("checked")); } } ]]></script> <toolbarbutton type="menu" label="menu"> <menupopup> <!--チェックボックス型メニューアイテム--> <menuitem type="checkbox" autocheck="false" label="menuitem (checkbox)" oncommand="checkStateHandler2(event);" /> <!--ラジオボタン型メニューアイテム--> <menuitem type="radio" autocheck="false" label="menuitem (radio)" oncommand="checkStateHandler2(event);" /> </menupopup> </toolbarbutton>
なお、XULの checkbox 要素および type=”checkbox” とした listitem 要素については、 autoCheck / autocheck 属性は存在しない。
Chrome Providers (content, locale, skin)
Chrome Providers (という言い方は今まで知らなかったが)には、 content, locale, skin の3種があることは XULチュートリアルの最初の方にも書いてあるように基本的な事柄である。 content には XUL や JavaScript といったメインソースコードが格納され、 locale には DTD や properties の言語リソースが格納され、 skin にはスタイルシートや画像が格納される。
バカ正直に拡張子 xul か js のファイルは content, dtd か properties なら locale, css か png か gif とかなら skin、と決め付ければいいのかと思いきや、必ずしもそうではない。ポイントは、 locale は Firefox 自体の general.useragent.locale の設定値によって内容が可変であり、 skin は Firefox 自体のテーマによって内容が可変である、ということである。
- ヘルプとして参照されるHTML形式のファイルがある。ヘルプの内容をローカライズ可能にしたいなら、 locale 配下に置けばよい。
- バインディングするための CSS がテーマによって改変されては困る。そういった CSS は当然 content 配下に置くべきである。
- 拡張機能のために作成したアイコンのデザインが気に入っているので、サードパーティーのテーマ作者が勝手に独自のアイコンに改変することを禁じたい。それなら skin に置くべきアイコン画像をすべて content に置けばよい。
- 「保存」ボタンに割り当てるアクセスキーを言語ごとに変えたい。英語なら Save なので S にしたいけど、ドイツ語なら Außer なので A にしたい。それなら XUL は <button accesskey=”&savebutton.accesskey;”> として、savebutton.accesskey を locale 中の DTD ファイルで定義してやればよい。こういったDTDの使い方は頻繁に見かける。
- 言語ごとにダイアログのサイズがフィットするようにしたい。それなら XUL を <window width=”&window.width;”> として、window.width を locale 中の DTD ファイルで定義してやればよい。こういうやり方は実際に chrome://browser/content/safeMode.xul などで見かけることができる。
2つの拡張機能でchrome URLがバッティングしたら?
すべての拡張機能は em:ID で一意に識別されている。
たとえば、ある拡張機能A(その em:ID は extension@example.com)がインストールされている状態で、別の拡張機能B(その em:ID は同じく extension@example.com)をインストールしたとする。たとえAとBが別物でも、 em:ID が同じ限りは共存できない。インストールしてFirefox再起動後、Aは消え去り、代わりにBが有効になる。
インストールされた拡張機能のパッケージ内のファイルは、 chrome URL によって一意に識別される。では、異なる em:ID を有する2つの拡張機能が同じ chrome URL を使用した場合、どうなるか?
ある拡張機能 foo がある。その chrome.manifest には、
content test jar:chrome/test.jar!/content/test/
と定義されており、 chrome://test/content/a.xul や chrome://test/content/b.xul が存在する。
まったく別の拡張機能 bar がある。その chrome.manifest には、foo と同様に
content test jar:chrome/test.jar!/content/test/
と定義されており、 chrome://test/content/b.xul や chrome://test/content/c.xul が存在する。
fooをインストール後、barもインストールした。
fooとbarは em:ID が異なるので、両者が無事にインストールされ、拡張機能マネージャにも表示されている。
chrome://test/content/a.xul は foo のものが表示された。
chrome://test/content/b.xul は bar のものが表示された。
chrome://test/content/c.xul は bar のものが表示された。
予想通りの結果となった。
DOMノードのアイソレート
ScrapBook の DOMイレーサー という機能はページ中のクリックした箇所を削除する機能であるが、右クリックによってクリックした箇所以外を削除するという裏技的な機能も有する。この機能を DOMアイソレータ と呼んでいる。
これまでのバージョンでは DOMアイソレータ の仕様は、クリックした箇所のノードを body の直下(つまり body.firstChild の位置)へ insertBefore し、 body の firstChild 以外の全ての childNode を removeChild するという処理の流れになっている。わかりやすく言えば、クリックした箇所のノードを切り離してトップレベルに持ってきて、それ以外を削除するということである。
しかし、このような処理ではCSSとの整合性が悪くなり、ページの見た目が崩れやすい問題があった。そこで、ScrapBook 1.0.12 (Build ID 20060625) では、body ~ クリックした箇所のノードまでの道は残して、道から外れている不要なノードを削除するような処理へと改変した。以下はその部分の処理を抜粋した関数である。
function isolateNode(aNode) { if ( !aNode || !aNode.ownerDocument.body ) return; while ( aNode != aNode.ownerDocument.body ) { var parent = aNode.parentNode; var child = parent.lastChild; while ( child ) { dump((child == aNode ? "o" : "x") + " " + parent.nodeName + " " + child.nodeName + " "); var prevChild = child.previousSibling; if ( child != aNode ) parent.removeChild(child); // 前のノードへ移動 child = prevChild; } // 親ノードへ移動 aNode = parent; } }
非同期ループ処理 (7) - 同期非同期複合型
これまで述べてきた非同期ループ処理の問題点として、 setTimeout の間隔を0ミリ秒にしたところで、全体としては単純な for ループよりもかなり時間がかかってしまうことである。
そこで、 setTimeout しない同期的なループ処理も組み合わせて適度に高速化を図る。
下記の例では3回の処理を一単位としている。
var syncAsyncProcessor = { _array : [], _count : -1, start : function(aArray) { // 開始処理 dump("start "); // 初期化 this._count = -1; this._array = aArray; this._next(); }, _next : function() { var elt = this._array.shift(); if ( elt ) { if ( ++this._count % 3 == 0 ) // 数回に一度、非同期 setTimeout(function(){ syncAsyncProcessor._process(elt); }, 1000); else // それ以外は同期 syncAsyncProcessor._process(elt); } else { setTimeout(function(){ syncAsyncProcessor._finish(); }, 1000); } }, _process : function(aElt) { // 処理 dump("processing (" + this._count + ")... " + aElt + " "); // 次の処理へ this._next(); }, _finish : function() { // 終了処理 dump("finish "); }, }; syncAsyncProcessor.start(['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']);
非同期ループ処理 (6) - 列挙型
配列でなく列挙型でも同じようなことができる。
下記の例ではブックマークのデータソースから全リソースを取り出し、リソースに対してなんらかの処理を行う。
var asyncEnumProcessor = { _enumerator : null, start : function(aEnumerator) { // 開始処理 dump("start "); // 初期化 this._enumerator = aEnumerator; this._next(); }, _next : function() { if ( this._enumerator.hasMoreElements() ) { var elt = this._enumerator.getNext(); setTimeout(function(){ asyncEnumProcessor._process(elt); }, 0); } else { setTimeout(function(){ asyncEnumProcessor._finish(); }, 0); } }, _process : function(aElt) { aElt.QueryInterface(Components.interfaces.nsIRDFResource); // 処理 dump("processing... " + aElt.Value + " "); // 次の処理へ this._next(); }, _finish : function() { // 終了処理 dump("finish "); }, }; var RDF_SVC = Components.classes['@mozilla.org/rdf/rdf-service;1'].getService(Components.interfaces.nsIRDFService); var dataSource = RDF_SVC.GetDataSource("rdf:bookmarks"); var resourceEnum = dataSource.GetAllResources(); asyncEnumProcessor.start(resourceEnum);
非同期ループ処理 (5) - 進捗表示2
非同期ループ処理 (4) と似ているが、配列から shift して要素を取り出すのではなく、配列全体を保持しつつ位置 _index を加算しながら要素を取り出している。この方法でもプログレスバーなどで進捗状況を表示できる。
var asyncProgressiveProcessor2 = { _index : -1, _array : [], start : function(aArray) { // 開始処理 dump("start "); // 初期化 this._array = aArray; this._index = -1; this._next(); }, _next : function() { if ( ++this._index < this._array.length ) { setTimeout(function(){ asyncProgressiveProcessor2._process(); }, 500); } else { setTimeout(function(){ asyncProgressiveProcessor2._finish(); }, 500); } }, _process : function() { var elt = this._array[this._index]; // 処理 dump("processing (" + (this._index+1) + "/" + this._array.length + ")... " + elt + " "); // 次の処理へ this._next(); }, _finish : function() { // 終了処理 dump("finish "); }, }; asyncProgressiveProcessor2.start(['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']);
非同期ループ処理 (4) - 進捗表示1
非同期ループ処理 (3) に、配列の長さ _length と現在の処理数 _count を追加した。
これによってプログレスバーなどで進捗状況を表示できるようになる。
var asyncProgressiveProcessor = { _count : 0, _length : 0, _array : [], start : function(aArray) { // 開始処理 dump("start "); // 初期化 this._array = aArray; this._count = 0; this._length = this._array.length; this._next(); }, _next : function() { var elt = this._array.shift(); if ( elt ) { this._count++; setTimeout(function(){ asyncProgressiveProcessor._process(elt); }, 500); } else { setTimeout(function(){ asyncProgressiveProcessor._finish(); }, 500); } }, _process : function(aElt) { // 処理 dump("processing (" + this._count + "/" + this._length + ")... " + aElt + " "); // 次の処理へ this._next(); }, _finish : function() { // 終了処理 dump("finish "); }, }; asyncProgressiveProcessor.start(['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']);
非同期ループ処理 (3)
非同期ループ処理 (2) とあまり変わらないが、次の処理へ進む部分を _next として切り分けており、より見やすくなっている。
var asyncProcessor = { _array : [], start : function(aArray) { // 開始処理 dump("start "); // 初期化 this._array = aArray; this._next(); }, _next : function() { var elt = this._array.shift(); if ( elt ) { setTimeout(function(){ asyncProcessor._process(elt); }, 500); } else { setTimeout(function(){ asyncProcessor._finish(); }, 500); } }, _process : function(aElt) { // 処理 dump("processing... " + aElt + " "); // 次の処理へ this._next(); }, _finish : function() { // 終了処理 dump("finish "); }, }; asyncProcessor.start(['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']);