読者です 読者をやめる 読者になる 読者になる

リンクになっていないURLを新規タブで開くGreasemonkey Script

JavaScript XPath Greasemonkey

Text URL Linker という Greasemonkey Script を CodeRepos (と Text URL Linker for Greasemonkey) にあげました。FirefoxGreasemonkey, Opera(9.50以降) の UserJavaScript, Safari の GreaseKit, Google Chrome の Greasemetal, ChromiumGreasemonkey で動作します。
はてなダイアリーのコメント欄など、リンクになっていないURL (一応 ttp://hoge のようなURLにも対応) を新規タブで開くリンク(通常のリンクと区別するため overline 入りカーソルをhelp) にする Script です。Firefox の人には Piro さんの Text Link でお馴染みのあれです。同種の Script (Greasemonkey だと Greasemonkey - Mozilla Firefox まとめサイト の Linkify Custom など)は結構ありますが、デフォルトではリファラを送信しない仕組みになっている、新規タブで開く*1のが特徴で、センタークリックでは通常のリファラ付きでURLを開きます。追記:tt25さんがCtrlキーを押している時は今のタブで開く修正をコミットしてくれました。
ただし、現状精度のほうはイマイチです。Wikipediaのように日本語を含むURLにも対応したかったので、URLの境界をかなり緩めにしています。これは後ろに余計なものがついた状態で開いてから不要な部分を削るほうが、後から必要な部分を足すより楽だという考えで実装しています。また、「部分的な強調などでテキストノードが分割されている場合」にも対応していません。


実装レベルの話ですが、XPathでテキストノードを取得し、DOM Range と createContextualFragment でテキストノードを Replace する処理をしています。こういったテキストレベルをあれこれする Script の模範的な実装(よりパフォーマンス重視な実装も可能ですが、堅牢性を考えるとこの辺りが落とし所という独断に基づきます。つっこみは歓迎です。)になっている。と思います。

というわけで、実装のポイントを軽く解説してみます。

descendant::text()[contains(self::text(),"ttp") and not(ancestor::a) and not(ancestor::textarea) and not(ancestor::script) and not(ancestor::style)]

まず、 descendant::text() はすべてのテキストノードです。それに対し、内で詳細な条件を指定します。
内の、contains(self::text(),"ttp") は ttp を含むテキストノード。 not(ancestor::TAGNAME) は TAGNAME を祖先に持たないという意味になります。a要素内のurlはすでにリンクになっていると思われるので、それらは選択しないようにしています。(祖先に持つべきでない要素は他にもあるかもしれません。)
処理対象のテキストノードだけをXPathで取得しているので、この時点でかなり効率的。なはずです。

  • DOM Range と createContextualFragment

document.createRange() で Range Object を作成し、テキストノードを selectNode (テキストノードを選択し、コンテキストを明示しておかないと、Operaはhtmlタグ、bodyタグを含む documentFragment を作成します。また、table,tr,tdなどの要素を選択した状態では勝手にcaptionタグを含めた documentFragment を作成してしまう(デモ:http://ss-o.net/test/createContextualFragment.html)ので、テキストノードを選択しておくと余計なタグのない documentFragment を作成できます) し、 createContextualFragment で documentFragment を作成します。
ダミーのspan要素などを作成して、そのinnerHTMLにテキストを流す方法もありますが、 createContextualFragment を使ったほうがすっきりと書け、パフォーマンスも良くなります。

var range = document.createRange();
range.selectNode(txt);
var df = range.createContextualFragment(newText);


その他、リンクにaddEventListenerするところはもう少しスマートにできないかと考えてはいるんですが、良いアイディアがなく getElementsByTagName で要素を取得して処理しています。正規表現なども改良の余地はかなりあると思うので、勝手ながらCodeReposでの改良に期待しています。

カスタマイズ

  • 見た目を変えたくない

初期設定では、テキストをリンクにするため通常の表示と見た目が変わってしまいます。ソースの TAG = 'a'; をHTMLで定義されていないタグ、例えば TAG = 'nota'; などにすると見た目を変えずにリンクにすることが出来ます。ただし、この場合キーボード操作することができなくなりますし、センタークリックも使えなくなります。

BLOCK_REFERRER = true; を BLOCK_REFERRER = false; にすればURLを開く際にリファラを送るようになります。

  • その他のスタイル

STYLE = 'cursor:help;display:inline !important;'; を編集すれば任意のスタイルでリンクを表示できます。初期バージョンでは text-decoration:underline overline; にしていましたが、現在のバージョンでは cursor:help; を指定しています。

*1:私はとにかく新規タブで開きたいのです。Operaの場合、タブを開くのも閉じるのもコストが低いので、ガンガン開いています。ちなみにカスタムで「戻るor閉じる」アクション(Back|Close page)を作ると便利です。