たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
ソース表示タブを更新して「やったー」と思ってたらFirefox 3では動くけどFirefox 2では動かないという状態になっていて焦った。0.2.*からFirefoxのブラウザのUIによりシームレスに統合するべく、「view-source-tab:(元のURI)」という独自プロトコルでソース表示するようにして、Chrome URLをユーザの目には見えないようにしてみたんだけど、この際に使ったリダイレクトの方法だとFirefox 3では動くのにFirefox 2では動かないという現象が起こってしまっていた。基本的には過去に訳した記事の通りにやったんだけど、思わぬ所で嵌ってしまった。
この情報、元々はURNサポートの実装を改善するために調べたんだけど、その時は、訳した記事の指示の通りにやるとブラウザ上で表示されるURIがリダイレクト前のURNのままになるのは望んでいなくて、結局nsIContentPolicyを使うようにしていた。
しかし今回はむしろその逆で、about:configみたいにユーザには実体のChrome URLを見せないまま機能させたいというのが目標だ。というわけで元記事のやり方ほぼそのままでnewChannel()で生成するURIだけChrome URLにしてみた所、Firefox 3でのテストでは問題なく動いたのでそのままリリースしたんだけど、これがFirefox 2では動かなかったという次第で。
viewSource.xul等を「リダイレクト先」にした場合、ブラウザ上に表示されるURIとしてはあくまでリダイレクト前の「view-source-tab:(URI)」という物になって、Firefoxからもあくまでそのリダイレクト前のリソースであるという風に見えるようになる。userChrome.cssでabout:configの表示をいじりたかったら 実体のChrome URLの方ではなく@-moz-document url(about:config) { ... }
と書かないと機能しない、ということからもそれが分かる。そのため、viewSource.xulにクロスパッケージオーバーレイで機能を追加していた場合、viewSource.xulにリダイレクトするとその表示結果のURIのもとではクロスパッケージオーバーレイは適用されず、タブの中でうまく動かすためのパッチを当てられないことになる。
しかしこの場合でも<?xul-overlay href="..."?>
で書かれていたオーバーレイは適用される。なので、ソース表示タブで用意したXULドキュメントからオーバーレイでviewSource.xulを読み込んでやれば問題なく動くようになる。というのがFirefox 3上でテストした時の結果だった。
Firefox 2では、クロスパッケージオーバーレイが効かないのはもちろんのこと、上記のように直接ヘッダ部分に書いて指定したオーバーレイも全然読み込まれないということが、リリース後に試してみてやっと分かった。ソース表示タブで用意したXULドキュメントを読ませても何もオーバーレイが適用されないので、画面は真っ白のまま……という状態になっていた。なんてこった。
ちょっと試した限りでは上手くやる方法が見つかりそうにないと思ったので、もう諦めて、Firefox 2の時はドキュメントのURIがリダイレクト先の物に完全に切り替わるnsIContentPolicyを使った方法を使うようにした。今のソース表示タブは、Firefox 3で「view-source-tab:http://...」などと入力するとそのURIのままでソースを表示するけど、Firefox 2の場合は「chrome://viewsourceintab/content/viewer.xul?http://...」という風にChrome URLが丸見えになる。その代わりちゃんと動く。という状態。
会社のサイトのコンテンツが一新され、コンテンツにブログが加わりました。何をやってる会社なのか(何ができるのか)分からんと言われがちなようなので、みんながちまちまやってる内容をここに書いていって、「当社ではこういうことができます」的な宣伝材料にする、というのが一応の目的のようです。
で、自分もアドオン開発者向けの自動テストツールであるUxUの使い方の解説なんかを書いてみました。ツールの使い方というよりは、「はじめての自動テスト」みたいな感じになってしまってますが。自分自身がまだ自動テストのことをよく分かってなくて須藤さんとかにしょっちゅう「何考えてんだ(全然分かってねえな)……」的な指摘を受けてばかりなので、自分の理解レベルに合わせるとこんな感じになってしまいました。技術力とか実績とかのアピールの場のはずなのに、こんな低レベルな記事を載せてしまうと却って会社の印象に泥を塗ってしまうんじゃなかろうか、ということをamachangのエントリとか見て思いもしましたが、もう後の祭りです。
UxUのマニュアルのつもりで、またそのうちXUL/Migemoのテストでの利用例の解説の続きを書くかもしれません。
あと、サービス→Mozillaサポート 実績紹介→Thunderbird用アドオンと辿ったページでThunderbird用の地味なパッチもいくつか公開しています。中にはもしかしたら同種のアドオンがすでに世の中にある車輪の再発明な物もあるのかもしれませんが、探すより作った方が早かったんで……
タイトルは釣り。
僕自身はなんだかんだで仕様原理主義者な所が今も強いわけで、その考えに則れば、onclick等のイベントハンドラは一応W3Cの仕様に含まれてるから(HTML4、XHTML 1.0、XHTML 1.1)OKだけど、ライブラリは業界団体の作る標準仕様になってないからNG、と言える。というのはまあ半分冗談だし、そもそもHolyGrailさんの指摘とは次元が違う話なのですが。
しかしこの考えも、権威主義だけじゃなくて、実利的に考えて「そうあるべき」と僕は割と真面目に思っていたりもする。
この三つの点について、満たしている物が多ければ多いほど、満たしているレベルが高ければ高いほど、それは良い物で学ぶ意義も大きいと僕は思う。
今でこそ僕はFirefox一辺倒だけど、これも「1」と「3」がそれなりに満たされているからという所が大きい。仕様は無いにしても実装がソースコードレベルで公開されているから、必要とあらば「こういう時にどうなるのか」をどこまでも追いかけられる。また、少なくともWindowsとMacとLinuxというマルチプラットフォームで使えて、どれか一つの環境で作り込んでおけば、他の環境でもそのまま使える(最悪でも、それほど大きな労力を掛けずにポーティングできる)。
そして上記三条件それぞれを最も突き詰めた物が、オープンスタンダードであり、デジュールスタンダードであり、デファクトスタンダードだ。
一昔前までは、ことWebについてはデファクトスタンダードとデジュールスタンダードが激しく乖離しているのが当たり前で、GeckoくらいしかまともにW3Cの仕様を実装している物が無い頃には「W3Cの仕様はWeb標準だからこれに則ってればいいことあるよ」と言っても「でもそれって夢物語じゃん」と返されるのが世の風潮だったと思う。でも今は時代が変わった。OperaもSafariも高いレベルでWeb標準に対応してきたし、IEも少しずつだけどWeb標準に合わせてきてる。今だったらはっきりとこう言える。「Web標準の技術を使っていれば、FirefoxでもOperaでもSafariでも動作するし、AirでもGoogleガジェットでもFirefoxの拡張機能でも使えるし、いいことだらけだよ」と。
不遇の時代を乗り越えて、「Web標準」は今、上記三つの条件を兼ね備えた物にようやくなりつつある。W3CやWHATWGやISOやECMAに仕様があって、仕様が公開されていて、しかもそれをほとんどのベンダがサポートしている。これって凄い事だと思わんかねぇ?(誰ともなく)
検索がヒットした箇所をハイライト表示していて「次候補」「前候補」を辿る時、フォーカスした要素をアニメーションで強調表示させる機能について、今までは簡易的な実装としてspan要素をposition:relativeにしてtopプロパティをいじることで「ぴょこん」とジャンプするような効果を付けてたんだけど、0.8.4でパクリ元のSafariと同じようなアニメーション効果(フォーカスされた箇所が一瞬拡大される)にするようにした。まああくまで擬似的な物なんですが。
検索にヒットした箇所にアニメーション効果を表示するやり方としては、canvasを使う方法など色々考えられましたが、思いつく限り最も単純なやり方で、要素をコピーして絶対配置するという方法で実装しました。これはText Shadowで折り返されたテキストに影を付ける方法を考えた時に思いついた手法の応用で、こんな風にしてます。
前のテキスト
<span style="position: relative;">
ハイライトされたテキスト
<span style="position: absolute;
top: -0.2em;
bottom: -0.2em;
left: -0.2em;
right: -0.2em;
font-size: 1.02em;">
ハイライトされたテキスト<!-- 複製されたノード -->
</span>
</span>
後のテキスト
position: relativeなインライン要素の中にposition: absoluteな要素を置くと絶対配置の基準がそのインライン要素になる、というCSSのポジショニングの特性を利用して、同じ位置に配置しています。また、top/bottom/left/rightの各プロパティにマイナスの値を設定することで、四辺が親のボックスより大きくなるズームっぽい効果が得られます。フォントサイズも一応いじってますが、あんまり分かりませんね。
手抜きなので、折り返された語句は正しく表示できません。あと、Safariみたいにアニメーションが終わった後もその箇所を特別に強調する、という効果は付けてません(そのうちやるつもり)。
もう一つ、0.8.5での改良点。0.8.2からSafari風強調表示を有効にした状態で半透明のスクリーンの下に隠れているリンクなどをクリックした時にクリックイベントを再送するようにしましたが、この半透明のスクリーンはクリックすると消えてしまうため、ミドルクリックなどで新しいタブでリンクを開いた時にも強調表示が解除されてしまうという欠点がありました。そこで、強調表示を解除しない例外的な操作の設定(正確には「この操作だった場合は一度消した強調表示を自動的に再表示する」という機能なんですが)をできるようにしてみました。
デフォルトでは、ミドルクリック、Ctrl-左クリック(リンクを新しいタブで開く)、Alt-クリック(リンク先を保存)、Shift-クリック(リンクを新しいウィンドウで開く)あたりの操作に対して、強調表示の状態を維持するように設定してあります。他のアドオンを使ってすべてのリンクを常に新しいタブで開いているようにしているから、そういうケースでも強調表示を解除しないようにしたい、という場合には設定をabout:configあたりで編集する必要があります。
この動作を決めている設定はxulmigemo.highlight.hideScreen.restoreButtonsという文字列型の設定です。値は「1,0+1,0+2,0+4,0+8,0+6,0+12」という風なカンマ区切りのリストになっていて、一つ一つが「この場合には強調表示を維持する」という場合の指定になっています。例えば「1」は「ミドルクリック」、「0+2」は「Ctrl-左クリック」を意味しています。プラス記号の左側はボタン番号(0=左クリック、1=ミドルクリック、2=右クリック)で、プラス記号およびその右側の数字はモディファイアキーの指定です(このパートは省略可能)。
モディファイアキーはnsIDOMNSEventの定数プロパティで定義されているフラグで指定します。Altキーは1、Ctrlキーは2、Shiftキーは4、Metaキー(MacのCommandキー)は8で、複数のキーを同時押しした場合を指定するにはそれぞれの数値を足した数を指定します。例えば「0+6」と書いた場合、プラス記号の右側の6は2と4の合計なので、「Ctrl-Shift-左クリック」の意味になります。このフラグ指定の意味がよく分からないという人はビット演算の話を見て下さい。
前のエントリの続き。
Safari風ハイライトに限らず元々、Firefoxの検索での「すべて強調表示」では、背景色と文字色を指定したspan要素を検索がヒットした箇所に動的に埋め込むという形で、ハイライト表示を実現している。これはinput要素やtextarea要素の場合でも全く同じ。実はFirefoxではテキスト入力欄もすべて、内部的には編集可能なHTMLとして実装されていて、それ故にspan要素の埋め込みも可能になっている。
ただ、この時span要素が埋め込まれる先のDOMツリーはchildNodesとかのプロパティでは辿れない場所にあって、アクセスするにはこんな風にする必要がある。
var editable = content.document.getElementsByTagName('textarea')[0];
var nodesInEditable = editable
.QueryInterface(Components.interfaces.nsIDOMNSEditableElement)
.editor
.rootElement
.childNodes;
textareaでこれをやってみると、改行が内部的にはbr要素で表現されているとかそういうのも見て取れる。nsIFindで検索する時はテキストフィールド内のこうした「隠しDOMツリー」も普通に検索対象になるようで、span要素を埋め込む時も特に変わったことはしなくていいようだ。
しかし、ここで一つ問題がある。こうしてテキストフィールド内に普通のspan要素を埋め込んでしまうと、その要素は、選択も内部の文字の編集もできない、ワープロでいえば埋め込まれた画像みたいな状態になってしまう。普段は特に意識せずに済むけど、常に強調表示を有効にするようにしていると当然テキストフィールド内でハイライト表示が行われることになる場合も多くなり、この問題が目につくようになってくる。というか僕自身がテスト用ドキュメントでテストしていて、いいかげんウザくなってきたのでなんとかしたかった。
理想的には、テキストフィールドにフォーカスされた時に自動的に強調表示を解除するという風な挙動にできるとよかったんだけど、試してみるとどうもなかなか大変そうだということが分かった。挿入されるspan要素にonclickなどの属性でイベントハンドラを設定してみたところ、getAttributeやdispatchEventなどのメソッド、あるいはparentNodeなどのプロパティを参照しようとするとパーミッションエラーが表示されてしまった。これではnode.parentNode.removeChild()
という風なことができないし、独自イベントを発行してChrome領域のスクリプトに後の処理を任せるということもできない。
これについては幸いにも、createRangeなどの機能は使うことができるようだったので、deleteContentsやextractContentsを使って自分自身を削除させるようにはできた。強調表示された箇所を選択して「選択範囲のソース」を表示してみれば、こんな風になっていることが分かると思う。
<h1>B.B.S. <span class="sub"><span onmousedown="
try {
var xpathResult = this.ownerDocument.evaluate(
'ancestor::*[contains(" INPUT input TEXTAREA textarea ", concat(" ", local-name(), " "))]',
this,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
if (!xpathResult.singleNodeValue) return;
}
catch(e) {
// permission denied, then this is in the input area!
}
var range = document.createRange();
range.selectNodeContents(this);
var contents = range.extractContents(true);
range.selectNode(this);
range.deleteContents();
range.insertNode(contents);
range.detach();
" id="__firefox-findbar-search-id" style="...">掲示板</span></span></h1>
無駄とは分かっていても一応、正攻法の判別処理も入れてある。テキストフィールド以外の部分に挿入されたspanにも全部このイベントハンドラが設定されてしまうのは、挿入先に応じて挿入する内容を変えるのがめんどかったから。まあとりあえず、これがあっても正常に動かなくなるわけではないし、別にいいかなと。
これだとキーボード操作に対して反応させることができない(そもそもカーソルをspanの中に移動できない)し、本当は特にクリック等の操作をしなくても、テキストフィールドにフォーカスが当たった時点で強調表示を解除するという風な挙動を実現したかったわけで、そこら辺まだまだ改善の余地はある。
一つ前のエントリの続き。
「すべて強調表示」の時に強調箇所以外を暗くするというSafari風ハイライト表示は、元はSafariHighlightを取り込ませてもらったものなんだけれども、基本的には、「画面の最全面に半透明の黒いボックスを表示して全体を覆う」「その上にz-indexを調整して強調箇所を浮かび上がらせる」という二つの操作が鍵になっている。で、このうち前者の方の操作のせいで、「暗くなった所にあるリンクをクリックしても、ハイライトが解除されるだけで、リンク先には飛べない」という問題が起こっていた。
まあ、問題というか実装上そうならざるを得ないという感じで、本家のSafariもこういう仕様だったと思うからまあいいじゃんと思わなくもないんだけど、要望はあるようなので対応しなきゃなーと思ってた。で、この度晴れて対応してみた。highlight.jsの最後の方に付け加えたresendClickEventメソッドがそれ。
resendClickEvent : function(aEvent)
{
var utils = aEvent.view
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
if ('sendMouseEvent' in utils) { // Firefox 3
var flags = 0;
const nsIDOMNSEvent = Components.interfaces.nsIDOMNSEvent;
if (aEvent.altKey) flags |= nsIDOMNSEvent.ALT_MASK;
if (aEvent.ctrlKey) flags |= nsIDOMNSEvent.CONTROL_MASK;
if (aEvent.shiftKey) flags |= nsIDOMNSEvent.SHIFT_MASK;
if (aEvent.metaKey) flags |= nsIDOMNSEvent.META_MASK;
window.setTimeout(function(aX, aY, aButton) {
if (ZoomManager.useFullZoom) { // Firefox 3のフルズームへの対応
aX = aX * ZoomManager.zoom;
aY = aY * ZoomManager.zoom;
}
utils.sendMouseEvent('mousedown', aX, aY, aButton, 1, flags);
utils.sendMouseEvent('mouseup', aX, aY, aButton, 1, flags);
}, 0, aEvent.clientX, aEvent.clientY, aEvent.button);
}
else { // Firefox 2, emulation
var args = [
'click',
aEvent.bubbles,
aEvent.cancelable,
aEvent.view,
1,
aEvent.screenX,
aEvent.screenY,
aEvent.clientX,
aEvent.clientY,
aEvent.ctrlKey,
aEvent.altKey,
aEvent.shiftKey,
aEvent.metaKey,
aEvent.button
];
window.setTimeout(function(aSelf, aFrame, aX, aY) {
var node = aSelf.getClickableElementFromPoint(aFrame, aX, aY);
if (!node) return;
var event = aFrame.document.createEvent('MouseEvents');
args.push(node);
event.initMouseEvent.apply(event, args);
node.dispatchEvent(event);
if ('focus' in node) node.focus();
}, 0, this, aEvent.view, aEvent.screenX, aEvent.screenY);
}
},
getClickableElementFromPoint : function(aWindow, aScreenX, aScreenY)
{
var accNode;
try {
var accService = Components.classes['@mozilla.org/accessibilityService;1']
.getService(Components.interfaces.nsIAccessibilityService);
var acc = accService.getAccessibleFor(aWindow.document);
var box = aWindow.document.getBoxObjectFor(aWindow.document.documentElement);
accNode = acc.getChildAtPoint(aScreenX, aScreenY);
accNode = accNode.QueryInterface(Components.interfaces.nsIAccessNode).DOMNode;
}
catch(e) {
}
var filter = function(aNode) {
switch (aNode.localName.toUpperCase()) {
case 'A':
if (aNode.href)
return NodeFilter.FILTER_ACCEPT;
break;
case 'INPUT':
case 'TEXTAREA':
case 'BUTTON':
case 'SELECT':
return NodeFilter.FILTER_ACCEPT;
break;
}
return NodeFilter.FILTER_SKIP;
};
if (accNode &&
accNode.nodeType == Node.ELEMENT_NODE &&
filter(accNode) == NodeFilter.FILTER_ACCEPT)
return accNode;
var doc = aWindow.document;
var startNode = accNode || doc;
var walker = aWindow.document.createTreeWalker(startNode, NodeFilter.SHOW_ELEMENT, filter, false);
for (var node = walker.firstChild(); node != null; node = walker.nextNode())
{
var box = doc.getBoxObjectFor(node);
var l = box.screenX;
var t = box.screenY;
var r = l + box.width;
var b = t + box.height;
if (l <= aScreenX && aScreenX <= r && t <= aScreenY && aScreenY <= b)
return node;
}
return null;
},
見ての通り、Firefox 3ではnsIDOMWindowUtilsの新機能を使ってイベントを発行するようにして根本的解決を図り、Firefox 2ではTabScope由来のコードにnsIAccessibleを組み合わせたものを使ってそれっぽいことをしている、という感じです。どっちもタイマーを使って処理を遅らせているのは、画面全体を覆っているスクリーンが消えてからイベントを発行しないと、もう一度スクリーンをクリックしたのと同じ事になってしまうから。
最初、nsIDOMWindowUtilsのsendMouseEventでclickイベントを発行しようとしてうまくいかなかったんだけど、どうやらこれはDOMのイベントをそのまま発行するわけではなく、本当の本当にユーザの操作をエミュレートするという物みたいだ。間を開けずmousedown→mouseupと実行するとクリックしたのと同じ事になる。
強調表示関係ではこのほかに、テキストフィールド内の強調を自動的に解除するようにするための改良もあります。これも長くなったから別エントリで。
pXMigemoFind(pIXMigemoFindの実装)、特にfindメソッドまわりを中心にだいぶ書き直した。といってもアルゴリズム的には変わってなくて、主にメンテナンス性を向上することを目的にした書き換えです。こういうのもリファクタリングと言っていいんでしょうか。
findInDocumentメソッドがかなり長くて中のループのネストも深くなってたので、これをだいぶ細かく分けた。
まず、フレームのツリーを辿る処理がループの最後の方にどかっとあったので、これを取り出してIteratorパターンのヘルパーオブジェクトとして実装することにした(最後の方にあるDocShellIteratorというやつ)。最初Iteratorパターンというのを知った時には「こんなの何の役に立つんだ?」とか「配列みたいに長さを取り出せた方が便利じゃね?」とか思ってたけど、こうして実際に書いてみると、「次に処理対象にする物を探す」部分だけに特化して作り込めるので便利だ、ということがよく分かった。
具体的には、今までは前方検索と後方検索それぞれの場合で「一つ前のフレーム」に処理を移すのか「一つ後のフレーム」に処理を移すのかをいちいち判別してたんだけど、その判別を行う部分まで含めてDocShellIteratorとして分離することで、findInDocumentの側では何も考えずにDocShellIteratorのiterateNextメソッドを呼べば適切な結果が帰ってきてウマーという風にできるようになった。
実際のコードで見ると、findInDocument側は
docShell = this.getDocShellForFrame(doc.defaultView)
.QueryInterface(Components.interfaces.nsIDocShellTreeNode);
if (aFindFlag & this.FIND_BACK) { // back
docShell = this.getPrevDocShell(docShell);
if (!docShell) {
if (!(aFindFlag & this.FIND_WRAP)) {
docShell = this.getDocShellForFrame(doc.defaultView.top);
docShell = this.getLastChildDocShell(docShell.QueryInterface(Components.interfaces.nsIDocShellTreeNode));
doc = docShell
.QueryInterface(Components.interfaces.nsIDocShell)
.QueryInterface(Components.interfaces.nsIWebNavigation)
.document;
this.document.commandDispatcher.focusedWindow = docShell
.QueryInterface(Components.interfaces.nsIDocShell)
.QueryInterface(Components.interfaces.nsIWebNavigation)
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindow);
if (
!editableInOut ||
findRange.sRange.startContainer == aDocument.body ||
findRange.sRange.startContainer == aDocument.documentElement
)
aFindFlag |= this.FIND_WRAP;
continue;
}
this.dispatchProgressEvent(found, aFindFlag);
break doFind;
}
}
else { // forward
docShell = this.getNextDocShell(docShell);
if (!docShell) {
if (!(aFindFlag & this.FIND_WRAP)) {
doc = Components.lookupMethod(doc.defaultView.top, 'document').call(doc.defaultView.top);
this.document.commandDispatcher.focusedWindow = doc.defaultView.top;
if (
!editableInOut ||
findRange.sRange.endContainer == aDocument.body ||
findRange.sRange.endContainer == aDocument.documentElement
)
aFindFlag |= this.FIND_WRAP;
continue;
}
this.dispatchProgressEvent(found, aFindFlag);
break doFind;
}
}
doc = docShell
.QueryInterface(Components.interfaces.nsIDocShell)
.QueryInterface(Components.interfaces.nsIWebNavigation)
.document;
if (doc == aDocument) {
this.dispatchProgressEvent(found, aFindFlag);
break doFind;
}
こーんなだったのが
aDocShellIterator.iterateNext();
if (aDocShellIterator.wrapped) {
if (!(aFindFlag & this.FIND_WRAP)) {
this.document.commandDispatcher.focusedWindow = aDocShellIterator.view;
if (
!editableInOut ||
!rangeSet ||
aDocShellIterator.isRangeTopLevel(rangeSet.range)
)
aFindFlag |= this.FIND_WRAP;
continue;
}
this.dispatchProgressEvent(aFindFlag, resultFlag);
break;
}
if (aDocShellIterator.isInitial) {
this.dispatchProgressEvent(aFindFlag, resultFlag);
break;
}
ここまでスッキリしました。まあその代わり、フレームのツリーを辿る処理(DocShellIteratorの方がちょっと長くなってしまったんだけど、それぞれロジック的には綺麗に分かれているから、今後手を入れる時は「ツリーを辿ること」と「検索すること」のどっちか一方のことだけに集中して作業できるわけで、これこそがイテレータパターンの最も大きな意義だったんですね。デザインパターン万歳。
あと、findInDocumentの中でループの中でループを回していた部分をfindInDocumentInternalメソッドとして分離した。これはロジックを分離する効果はなくて、単にネストを浅くしてコードを見やすくするためだけの変更。のつもりだったんだけど、これをうまくやるために、他の部分も含めてだいぶ手直ししないといけなかった。
何故かというと、単純に内側のループだけをfindInDocumentInternalとして分離したところで、元のfindInDocumentとfindInDocumentInternalとの間でお互いに引き渡さないといけない情報(引数として引き渡す情報と、返り値として戻す情報の両方)が多すぎて、そのまま二つに分けただけだと却って分かりにくくなってしまったから。普通にreturnするだけじゃ情報を渡しきれないから、オブジェクトを引数で渡して、そのオブジェクトのプロパティとして返り値を設定して、変更されたプロパティを呼び出し元の側でまた参照して……という感じになってしまって、せっかくメソッドを分けたのにそこの所でガッツリ繋がってしまってて、これじゃ分けた意味がないよと。
というわけで最低限必要な情報だけに絞り込んでやりとりするように設計を検討したのに加えて、インターフェースの定義も見直して、今まであんまり有効活用してなかったビット演算を多用するようにしたことで、findInDocumentInternalからの返り値は最終的にビット列一つだけにまでまとめることができた。ビット演算いいよビット演算。
ビット演算を有効に利用するためには、どの意味をどのビットに与えるかというのをよく考えておかないといけない。とかなんとか大仰な言い方をしてみたけど、要するに、適当に定数プロパティを名前順に並べて先頭から0, 1, 2, 4, 8……と値を割り振っていくなんていいかげんな事してちゃ駄目だよってことですね。0.8.1まででは
const unsigned short FOUND = 0;
const unsigned short NOTFOUND = 1;
const unsigned short WRAPPED = 2;
const unsigned short NOTLINK = 4;
const unsigned short FOUND_IN_EDITABLE = 8;
こんなだったけど、これじゃあ実質的にはただの定数値が並んでるだけで単純比較にしか使えない。FOUNDが0だったり「検索はヒットしたけどリンクじゃないからヒット無しとみなす」という意味のNOTLINKなんてのがあったり。一つの値に複数の意味が割り当てられてるんじゃ、重複しないビットを割り当てても意味がないわけです。
const unsigned short NOTFOUND = 0;
const unsigned short FOUND = 1;
const unsigned short WRAPPED = 2;
const unsigned short FOUND_IN_LINK = 4;
const unsigned short FOUND_IN_EDITABLE = 8;
こーいう風にしておけば、NOTLINKに相当する場合は「FOUND | WRAPED」、そうでない場合には「FOUND | FOUND_IN_LINK」とかそんな風に書けるわけで、これなら「普通にヒットした」「普通にヒットしたけど一度ページの末尾まで検索してもう一度頭からやり直した」「入力欄の中でヒットした」などなどいろんな場合もひっくるめて全部「flag & FOUND」というビット演算いっこで判定できる。
あとSafari風強調表示の改善もあるんだけど、長くなるから別のエントリに分けます。
regression(後退バグ。修正のために加えた変更が原因で新たな問題が発生すること)のせいで体力・精神力を消耗する事が重なり、自動テストドリブンな開発の重要さを身に染みて感じた。プレゼンでも言ったけど、「やってりゃ良かった」と後悔してばかりだ。まさかこんなに手こずる羽目になるとは当初は思っていなかったから。
自動テストはregressionの発生を防ぐ(正確には、regressionを残したままでいることを防ぐ)素晴らしいメソッドだ。と今になって改めて思う。
でも、どんなコードでも機械的に自動テストにかけられるわけではない。機械的に自動テストを実施するには、自動テストを実行しやすい設計になっていなければならない。自動テストを実行しやすい設計とは、粒度が小さい=オブジェクトやらメソッドやらが可能な限り細かい単位で分割されている設計のこと。テスト対象となる部品自体が可能な限り自己完結していて、外部的な要素は必要に応じてすべてパラメータとして与えるようになっていること。横着して一つの関数内からグローバル変数やら何やらを参照しまくっていると、その関数のロジックそのものをテストすることができない。
そういうわけで、自動テストドリブンな開発には気を遣わなければならない。気を遣わないといけないから気力を消耗する。だから、せずに済むのなら、しないままでいたい。でも、そう考えているうちに「自動テストを作成する&テストを実施しやすい設計にするコスト」と「やっつけで作って、その都度メンテナンスするコスト」の関係が、「前者>後者」だったのがいつの間にか「前者<後者」に逆転してしまうようになっていて、メチャメチャ後悔することになる。
今まで自分のやってきたことは9割方、自動テスト無しでもどうにかやってこれていたし、そもそも、自動テストの重要性とそのための設計の指針が分かった今改めて見返してみても、自動テスト化することが困難な物が多かった、と思う。だから、自動テストを前提にしてコードを書くという習慣が身についていない。自動テストが最初から不可能な事が多かったから、自動テストのできない設計にするしかなかったから、自動テストのできない設計にすることが当たり前になってしまっている。
でもいい加減、その悪習を断ち切らなければならない。25歳ももうすぐ終わりの、四捨五入すれば三十路の、今更も今更で手遅れ感がとても強いけれども、やらなければいけない。今まで自分がやってきた方法は通用しないということを自覚しないといけない。できて当たり前の事が今まで全くできていなかったという事、本当はこの面子の中で飛び抜けて一番遅れているという事、それなのに対等であるように勘違いして思い上がっているという事、今まで他人事だと思ってた「ダメな人の典型」に自分がまったく当てはまっているという事、全部認めないといけない。
そういうわけでとりあえずUxUはマジオススメ。
Firefox 3のFUEL、調べれば調べるほど酷さが浮き彫りになってくる。norahさんが「これはひどい」と言ってた意味を! 「心」でッ! 「理解」したッ!! いやもうほんと酷すぎます。XULとJavaScriptとXPCOMが入り乱れてる酷い現状を何とかしようとして作られたはずなのに、それで出てきた物が輪をかけて酷いなんて、悪夢だ……
正直、こんな物の使い方の解説を書くのは犯罪じゃないかって気がするよ。使い方とか誰にも知られないまま、ひっそりと廃れるかひっそりと改善されるのを待つか、どっちかにしたほうがいいって。まじで。
例えばこんな世界を想像してごらん。
var listener = new EventListener();
var reference1 = document.getElementById('item');
reference1.addEventListener('click', listener, false);
var reference2 = document.getElementById('item');
reference2.removeEventListener('click', listener, false); // ここでエラーになる。
alert(reference1 == reference2); // false
これが実際に起こるのがFUELの世界なんだぜ……
var reference1 = Application.extensions.get('myextension@mydomain');
reference1.storage.set('privateValue', 'someData');
var reference2 = Application.extensions.get('myextension@mydomain');
var val = reference2.storage.get('privateValue', 'defaultData');
alert(val); // says "defaultData"
alert(reference1 == reference2); // false
誤解してた。Extensionとそのsotrageプロパティの場合は問題ないようだ。でもWindowやBrowserTabへのイベントリスナの登録はやっぱり問題あり。こういう風に「どの場合は問題なくて、どの場合は問題ありなのか」が場合によって異なるというのも非常に困る。
もちろんこの状況でも問題を回避する方法はあるけど、その方法の意味を理解するには、処理系の中で何が起こってるかが分かっている必要がある。そういうことが分からなくても気楽に書けるのがLightweightLanguageのいいところなんじゃないのかー!
各地既報ですが、セキュリティ上の仕様変更によってmozIJSSubScriptLoaderで読み込めるスクリプトの置き場所がChrome内に限定されるようになった(File URLなどの読み込みは拒否されるようになった)そうで。まあ自分が作ってる拡張機能には外部のスクリプトを動的に読み込んでどうこうするものがあんまり無いのでそれほど影響は無いんですが、せっかくなので手持ちの情報を晒しときます。
実は僕、割と最近まで、mozIJSSuScriptLoaderで実行コンテキストを指定できるってことに気付いてなかったんですよね。だから、外部スクリプトは実行できてもその実行結果を取り出すとかはできないと思い込んでまして。Firefox 1.5以前から作ってたやつでデフォルト設定をdefault.jsとしてcontent内に置いてた物で、default.jsの内容を読み込ませるために、わざわざmozIJSSubScriptLoaderと同じ働きをするコードを書いてたんです。ああ車輪の再発明。そんなわけで以下の情報がもしかしたら参考になるかもしれません。
……あ。今気付いたけど(ぉぃ)、Chrome URL限定ってことはdata: URLもダメになったってことか。んじゃUXUもやっぱ影響受けるなぁ。さてどうしたものか。
とりあえずスクリプトを実行するだけだったら上記の方法で読み込んだファイルの内容をeval()
するだけでいいので、その点では話は簡単なんですけどね。
20日追記。最終的に、格好悪いやっつけの方法ではあるんだけど、こういう風な所に落ち着いた。UxU 0.2.6で採用した方法は以下の通り。
eval()
するだけのスクリプトをパッケージに含めておき、mozIJSSubScriptLoaderで読み込んで実行。実際のコードはこんな感じ。
var loader = Components
.classes['@mozilla.org/moz/jssubscript-loader;1']
.getService(Components.interfaces.mozIJSSubScriptLoader);
this.include = function(aSource, aEnvironment, aEncoding) {
var encoding = aEncoding || this.getPref('extensions.uxu.defaultEncoding')
var script = this.readFrom(aSource, encoding) || '';
var env = aEnvironment || this.environment;
env._subScript = script;
loader.loadSubScript(
'chrome://uxu/content/test/helper/subScriptRunner.js?includeSource='+
encodeURIComponent(aSource)+
';encoding='+encoding,
env
);
};
subScriptRunner.jsというファイルの内容は、たったこれだけ。
if (_subScript) eval(_subScript);
これで、mozIJSSubScriptLoaderで普通にスクリプトを読み込ませるのと同じような結果になる。
さらに追記。仕様が変更されて、file:とresource:は使えるようになったそうだ。でもdata:は相変わらずダメなので、今までdata:でやってたものはここに書いたような何らかの方法で代替するしかないと思う。
さらにさらに追記。evalInSandboxを使う方法もあるそうだ。
さらにさらにさらに追記。evalの機能についてFirefox 3.1でまた変更があったようだ。