たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
表題の通り、XUL/Migemo 0.10.5からロケーションバーでNOT検索を可能にしました。「mozilla -firefox」という風に入力すると、「Mozillaという単語は含むがFirefoxという単語は含まない」候補だけがヒットするようになります。当然Migemo検索との併用も可能。いらん候補が大量にヒットするのがウゼーと常々思っていたので、バグ修正のついでにサクッと実装してみました。
残念ながら、履歴とブックマークの管理、履歴サイドバー、ブックマークサイドバーでの検索については非対応です。これらの検索機能はPlacesのクエリ機能に依存していて、そのクエリ機能にNOT検索の機能がないためです。クエリにNOT検索の機能が付いたら対応できるかも。
ということでXUL/Migemo 0.10.0やっとリリース。
スクリーンショットを見ると分かるとおり、なにげにAND検索にも対応してます。仕掛けは単純といえば単純で、入力された文字列をスペースで区切ってそれぞれについて正規表現を生成した後に、それらの順列組み合わせを全展開した正規表現をさらに生成する(これを使ってマッチングするので、各単語の順番が入れ替わってもマッチする)という、ものすんごい力業。単語数が増えると組み合わせの数が爆発的に増えて正規表現がクソ長くなってマッチングがクソ重くなるので、実用的な速度が出るのはだいたい3語くらいまでが限界だと思います……
ちなみに順列組み合わせの展開には無駄にMozStorageを使ってます。JavaScriptで一体どーいうアルゴリズムでやりゃぁいいのかちっとも分からんかったのでググってみたら、順列組み合わせの総数を求める方法ばっかり引っかかる中で一つだけSQLの自己結合を使った解き方が見つかったので、それをそのまま使わしてもらいました。pIXMigemoTextUtilsのgetANDFindRegExpFromTermsというメソッドがそれなので、興味ある人は見てみてください。
しかしまあ、今回のこれはFirefox 3 HacksのためにPlacesのことを詳しく調べてたからやっと実現できたようなもんで、つまりFirefox 3 Hacksを読めばこんなことは楽勝でできるようになるかもねということで、皆さんゼヒ買って下さい、と宣伝しておきます。オライリーから8月発売予定です(再掲)。
5日追記。ページ内検索では考慮しなくてもよかった問題が表面化してフリーズする(全角スペース1文字だけにヒットした場合に無限ループに陥ってしまう)という現象が起こってしまっていて焦った。対策を入れて早速更新した。
検索がヒットした箇所をハイライト表示していて「次候補」「前候補」を辿る時、フォーカスした要素をアニメーションで強調表示させる機能について、今までは簡易的な実装として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風強調表示の改善もあるんだけど、長くなるから別のエントリに分けます。
好きなように正規表現で検索できるようになりました。XUL/Migemo 0.8.0からの新機能ということになります。というかAPIとしては持ってたのにUI上から利用する手段が今までなかった。
「普通の検索」と「正規表現」と「Migemo」の3モードが排他的な切り替えということになるので、UIをどうするか迷って、チェックボックスを増やしてみたりドロップダウンリストを置いたり色々試してみたけど、最終的にはラジオボタンを並べる形に落ち着いた。
使う側としてはややこしくなってしまったので、せめてちょっとでも使い勝手をよくしておきたいと思って、キーボード操作だけで検索モードを切り替えたり、入力内容が /ほげほげ/ みたいな正規表現リテラルっぽい形だったら自動的に正規表現検索に切り替えたり(判別にはNarcissus由来の正規表現を使わせてもらいました)、という風な工夫も入れてみたけど、実際の所どうだろう。
という外見でよく分かる変更の他に変わった所では、後方検索の仕組みを今までとガラッと変えてみた。
元々の作者のplus7さんはどうされていたかというと、正規表現のマッチングで後方検索ができないから、「正規表現の内容を前後逆にひっくり返して」「検索対象の範囲のテキストも全部反転して」その上でマッチングを行う、という方法を採っていて、僕が引き継いだ後もそこは変わりなかった。
var regexp = /(ほげ|hoge|foo|bar)/;
var text = 'ほげほげ、hogehoge。foo? bar。';
// ここで、「bar」「foo」「hoge」「hoge」
// 「ほげ」「ほげ」の順に検索したい
regexp = regexp.compile(
regexp.source
.split('')
.reverse().join('')
.replace(/\)/g, '[[')
.replace(/\(/g, ')')
.replace(/\[\[/g, '(')
);
text = text.split('').reverse().join('');
var found;
while(text.match(regexp))
{
found = RegExp.lastMatch.split('').reverse().join('');
alert(found);
text = RegExp.rightContext;
}
(なんでこんなややこしいことをするのかについてはplus7さんの技術解説を参照のこと)
しかし、この度正規表現検索機能を表に出すにあたって重大な問題が発覚した。元の「正規表現を反転する処理」は、反転しなきゃいけない正規表現がXUL/Migemo自身の生成したごく単純なパターンの物だけだから、かなり簡単な作りになっていた。でも、ユーザが手入力で凝った正規表現を入力すると、途端に処理がうまくいかなくなってしまう。
これは非常にまずい。ということでなんとかならないかと考えてみた。
まず、正規表現を反転する処理をもっと改善する事を考えてみた。でもこれは早々に諦めた。調べてみればみるほど、正規表現という物は複雑怪奇な書き方ができるようで、その全部の記法に対応するなんでのはとてもじゃないけど僕の頭じゃ無理。
となると残された道としては、正規表現も検索対象の範囲のテキストも反転せずにどうにかして後方からマッチングを行う方法を見つけるしかない。
んでJavaScriptのリファレンスを見ながらうんうん考えてたんだけど、ふと思いついた方法を試してみたらなんとバッチリうまくいった。そのやり方というのは、「g」フラグを使うというもの。
gフラグを付けてマッチングを行うと、「最後にヒットした箇所」はマッチングの結果の配列の最後の要素として取得できる、というのは当たり前。でもそれだけじゃなくて、実験して確認してみたら、この時のRegExp.leftContext
とRegExp.rightContext
は、gフラグ無しの時は最初にマッチした箇所から見た「それより前」と「それより後」だったのが、今度は、最後にマッチした箇所に対する物になっていた(よく考えてみれば当たり前のことだけど、今まで全然気付いてなかった)。
ということは、このleftContextを次の検索のマッチング対象にし、そこでもまたgフラグを使って、さらにその時のleftContextを次の検索のマッチング対象にして……という風に繰り返していけば、「正規表現で後ろから検索」するのと同じ結果が得られるわけだ。
var regexp = /(ほげ|hoge|foo|bar)/;
var text = 'ほげほげ、hogehoge。foo? bar。';
regexp = regexp.compile(regexp.source, 'g');
var found;
while(text.match(regexp))
{
found = RegExp.lastMatch;
alert(found);
text = RegExp.leftContext;
}
というわけで無事、どんな正規表現でも問題なく後方検索にかけることができるようになった次第です。しかも文字列の分割とか連結とか配列の反転とかいろんな処理がゴッソリ不要になったから、長いページでは後方検索がちょっと高速になったんじゃないかと思う。
やっとBeta5準拠のデザインで巻き戻し/早送りボタンのアイコンを作り直したぜKeyholeにもばっちりたいおうだぜ、と思った瞬間にデザイン変更が入って僕涙目。
各ボタンの位置が2ピクセルずつ変わってるとかの地味な変更が。これはイジメだろう……
Firefox 2まででは、position:fixedな要素はz-indexを適切に指定すれば内容領域の上にも普通に表示できた。なので、ウィンドウ全体を覆い尽くすというのも簡単だった。
でもFirefox 3では仕様が変わって、常にサブフレームの内容が上に表示されるようになったため、CSSのポジショニングだけでは任意の要素を任意の位置に表示することはできなくなってしまった。仮にposition:fixed; top:0; left:0なボックスを置いても、その上にブラウズ領域の中身が表示されてしまう。ウィンドウの内容の上に何かを重ねて表示したい時には汎用のポップアップであるpanel要素を使え、というのがFirefox 3流のやり方らしい。
しかし古い拡張機能をアップデートするにあたってそこらへんのつくりを全面的に書き換えるのは大変な労力を要するし、果てしないregressionの嵐が発生しかねない。できれば最小限の労力で、今までのやり方に一工夫加えるだけで正常に動作するようにしたい。
ということでウィンドウ全体を覆い隠してゴニョゴニョするアレの時に試行錯誤して、CSSのポジショニングをどーしても使いたければサブフレームを新たに生成するしかないっぽいという結論に辿り着いていただけれども、その方法をタブカタログで使おうとしたところDOMDocumentの違いとかで死にそうになったので、諦めて別の方法をいくつか探ってみた。
で、結論としては、visibility:hiddenになったサブフレームの上なら、普通にCSSのポジショニングで要素を重ねられるようです。display:noneとかvisibility:collapseとかをサブフレームに使うとDocument Shell(nsIDocShellのオブジェクト)がぶっ壊れてしまってまともに動かなくなるので、この辺のプロパティをいじるのはヤバイと思って今まで全く触ってなかったんだけど、少なくともFirefox 3ではFirefox 2でもFirefox 3でも、visibility:hidden/visibleの切り替えではDocument Shellは破壊されないようだということに、ああでもないこうでもないといじってるうちにたまたま気がついたのですよ。
でもこれだとタブカタログのように、一旦全面を覆い尽くしてその上に全然別のコンテンツを表示するといった機能は作れるけど、InterNoteのようにページ上に任意の要素を置くっていうことはできそうにない。そういう場合はページ内のDOMDocumentを編集するしかないようだ。
ブックマークまわりの仕様が変わってるから手こずりそうだなあと思ったけど、案外サックリ行けた。むしろPlacesベースの今の方が、ゼロから作るなら簡単に作れそうな勢いだった……
下のペインにフォルダが出るようにするかどうかで迷って、今は下のペインではフォルダは全く出さないようにしてしまった(Firefox 3のみ)。選択されたフォルダの直下にあるフォルダだけをレンダリングさせる方法が分かればそうしたい。