Subscribed unsubscribe Subscribe Subscribe

枕を欹てて聴く

香炉峰の雪は簾を撥げて看る

Minibuffer からはじめるGreasemonkey Script

20080406 21:50 キーコードについての説明を追記しました。
Greasemonkeyを使ってる人ならたいていの人が知ってるMinibufferとLDRize(偏見?)
ただ、なんというかMinibufferについての説明がMinibuffer 2007.11.15 の変更点 | 3.14silog - script/LDRizeの記事くらいしかなく、半ばソース読んで書け状態になってるので結構ハードルが高いのではないかと。
そこで、自分でもDeleteCommand for GreasemonkeyっていうMinibuffer上で動くGreasemonkeyを書いたんだけれど、そのときにわかったこととか、後でMinibuffer用Scriptを書くときの備忘録みたいなのをまとめておく。
これよんでちょっとでもGreasemonkey、またMinibufferに興味持つ人が増えないかな。なんてことを思ってます。
「これはないわ。」とか「わかってねーな。」ってところがあったら指摘してもらえるとうれしいです。
Minibuffer用Script書いてみようかなっていうふうな気になった方がいればうれしいです。

Minibufferとは

Minibufferっていうのはid:snj14さんの書いたScriptでLDRizeやReblogCommandの基盤部分になってるScriptです。最新版はCodeReposからDownloadできます。

どんな感じ?

MinibufferはあらかじめwindowオブジェクトにMinibufferオブジェクトをつくり、そこにAPIを格納しています。ここに格納されている関数はwindowオブジェクトを介して、ほかのGreasemonkey Scriptで使用することができます。evalとかで無理やり取ってこなくても大丈夫です。
ここには様々な便利な関数と、Minibuffer本体に処理を追加するための関数が用意されています。
これによって、Minibufferが作動している場合、簡単にショートカットキーで処理を行うのを付け加えたり、Minibufferのコマンドを追加したりすることができます。さらにLDRizeが作動している場合、LDRizeによってPinをうたれたアイテムや、現在LDRizeが指しているアイテムに対して、処理をするコマンドを簡単に追加することができます。便利ですね!

実際の活躍

ここでMinibufferが現在どんなことで役に立っているかをみていきます。
ReblogCommand | 3.14
id:snj14さん作。TumblrのReblogをショートカットキー一発で行うものです。Tumblrを知らない方は「何それ?」って思うかもしれませんが、Tumblrで正式なReblogの方法を使ったことのある方はショートカットキー一発の偉大さがわかると思います。もはやTumblrやってる方にとっては常識ともなりつつある(偏見?)Greasemonkey Scriptです。

/lang/javascript/userscripts/minibufferbookmarkcommand.user.js – CodeRepos::Share – Trac
id:snj14さん作。ショートカットキー一発でSBMにブックマークするものです。Tagもつけられます。

という風にものすごく便利なわけです。

どんなのなのか見てみよう

では本題。Minibufferってどんなのなのか見てみましょう。最新版の1493行目からwindow.Minibufferを見ることができます。

  • getMinibuffer : Minibufferのオブジェクト本体を得ることができます。
  • getShortcutkey : Shortcutkeyオブジェクトを得ることができます。
  • addShortcutkey : 引数に指定したものをcommand.addShortcutkeyに渡しています。今はさわりだけなのでどんどんみていきます。
  • addCommand : 引数に指定したものをcommand.addCommandに渡しています。これも後々見ていきます。
  • execute : コマンドの名前と、処理対照のnodelistを渡すと、処理を実行してくれます。
  • message : 表示するHTMLをStringにしたものと、秒数を渡すと、messageを表示するようです。
  • status : 引数は(name, status, timelimit)の三つ。この三つでStatusオブジェクトを作るようです。どんどんみていきます。
  • $X : かの有名な(自分の中では特にお世話になっている)$X関数がMinibufferのプロパティとして定義されています。$Xについてはすぐ後で見ていきます。すごいです。
  • $N : $N関数です。これもものすごく便利です。すぐ後で見ていきます。
  • D : DはJSDeferredのことです。今見るまでMinibuffer上で定義済みだったなんて知りませんでした。

ぱっと見はこれだけだと思います。まず、Minibuffer本体から遠いところからアプローチしていきます。

$X、$N関数

1111行目から始まっているのが$X関数です。id:cho45さんの作ったものです。
ものすごくはしょって簡単に言うと、XPathを投げると、それに一致した要素を配列で返してきてくれる関数です。getElementsByTagNameのXPath版なのですが、XPathは柔軟な表現が可能なため簡単に絞り込むことができます。
XPathっていうのはAutoPagerizeとかLDRizeとか拙作のLDRFullFeedなどで使われているあれです。AutoPagerizeで一気に有名になったので、Googleとかで調べてもらうとすぐ出ると思います。
コメントに

// $X
// based on: http://lowreal.net/blog/2007/11/17/1
//
// $X(exp);
// $X(exp, context);
// $X(exp, type);
// $X(exp, {context: context,
// type: type,
// namespace: {h:"http://www.w3.org/1999/xhtml"}});

と説明が書かれています。expとはXPathの表現で、contextにはXPathのコンテキストノードを入れる。XPathについては詳しいところはいろいろありますので調べてみてください。最近XPath大人気だなあ。

とりあえずGoogleあたりで一度使ってみましょう。Minibufferを入れておいて、それより後に次のGreasemonkeyを入れて、Googleで検索してみてください。

// ==UserScript==
// @name           Sample1: Google Highlighte
// @namespace      http://d.hatena.ne.jp/Constellation/
// @include        http://www.google.*/*q=*
// ==/UserScript==

// Minibufferが動いているかどうかチェック。なければreturnでScriptを中止させている。
if(!window.Minibuffer) return;

// 使いやすいように、$X関数を参照する
var $X = window.Minibuffer.$X;

// XPath
var xpath = '//table/tbody/tr/td/div/b';

// document要素をコンテキストノードとしてXPathに当てはまった要素を
// 配列にしてそれをboldsで参照
var bolds = $X(xpath, document);

// 配列をまわして、それぞれの要素に背景色をつける。
for (var i = 0, l = bolds.length; i < l; i++ ){
  bolds[i].setAttribute("style", "background:#fff8c1 !important");
}

うまくいきましたか?$X便利すぎです。(CSSでもっと簡単にできるよとかいうのは今はあえて無視します。)

$N関数は1086行目から始まります。$N関数もすごくはしょっていうと、新しく要素を作るための関数です。新しく要素を作るための定義を投げれば、要素を作ってくれます。
1086行目だけ借りてきました。

function $N(name, attr, childs) {

引数はnameとattrとchildsです。

  • name : これから作る要素の名前です。要するにdivとかaとかtableとかいうあれです。
  • attr : オブジェクトを投げます。こんなかんじです。
var attr = {
  id : "box",
  style : "boder:1px solid;background:#fff8c1;z-index:1000;"
}
$N('div',attr)

ちなみにchildsにStringを投げるとtextnodeとして、nodelistを投げるとそれらを子要素として追加するようです。$Nは便利ですが、そんなに長いものでもないのでぜひみてみることをお勧めします。

JSDeferredについてはまだあまりよくわかっていません。すいません。
同期的に処理をきれいに書くことができるようです。
GM_xmlhttpRequestは非同期なので、取得後の動作はどうしてもoptionのonload以下に続けないといけないのですが、これを使うともっときれいにかけるようです。Deferredの解説は、Googleあたりで検索すると出てきます。投げやりですいません。

Minibufferに処理を追加する。window.Minibuffer.addCommand編

ここまではMinibufferを単なる便利な関数の倉庫として扱ってきましたが、ようやく処理を追加していこうと思います。

ここでとても参考になるのが1509行目から始まるコマンド登録の羅列です。これを見るとわかるように、Minibufferに標準で登録されているコマンドは、addCommandをつかって登録されているようです。

とりあえずひとつ適当に作ってみました。

// ==UserScript==
// @name           Sample2 :Show Message
// @namespace      http://d.hatena.ne.jp/Constellation/
// @include        http://search.yahoo.co.jp/search?*
// ==/UserScript==

// Minibufferが動いているかどうかチェック。なければreturnでScriptを中止させている。
if(!window.Minibuffer) return;

window.Minibuffer.addCommand({
// 追加するコマンドの名前
name: 'Show::location',
// 追加するコマンド
command: function(){ 
window.Minibuffer.status('Sample','Google?...');
setTimeout(function(){window.Minibuffer.status('Sample','Google?...Yahoo!',150);},1000);
},
});

Yahoo!で検索して、Alt-xでMinibufferの画面を呼び出し、Show::locationと入力してください。うまくいくかな?

window.Minibuffer.statusについての説明。
window.Minibuffer.statusは新しくStatusオブジェクトを作ります。
引数は(name, status, timelimit)の三つ。

  • name そのステータスメッセージの名前です。nameの値を同じにしてwindow.Minibuffer.statusを呼ぶと、そのメッセージの内容が更新されます。
  • status 文字列を入れると、その文字列がメッセージとして表示されます。
  • timelimit 数字を入れると、何秒後にそのメッセージが消えるのかが決まります。これを省略すると、メッセージは消えなくなります。

Sampleの場合は
window.Minibuffer.status('Sample','Google?...');
でSampleというnameのステータスに'Google?...'というメッセージを表示しています。timelimitは省略したので、このままでは消えません。
そこでsetTimeoutで1000msec待って
window.Minibuffer.status('Sample','Google?...Yahoo!',150);
を呼んでいます。nameが先ほどと同じSampleなので、先ほどのステータスが更新され、
メッセージが'Goolgle?...Yahoo!'に、そして150msec後に消えるように変化しました。
きちんと消えましたか。

addCommandにはオブジェクトを渡します。

  • name : そのコマンドの名前です。Minibufferでこの名前を入力すると実行されます。
  • command : そのコマンド自身です。commandには引数がひとつ渡されます。一般に(Minibufferの中身によると)stdinで受けます。今回は使用しませんでした。

ちなみにここで登録したコマンドは後々window.Minibuffer.executeでScriptから実行できます。

次はショートカットキーを設定します。

Minibufferに処理を追加する ショートカットキー編

ショートカットキーを登録する方法は2つあります。

  • window.addShortcutkeyを使う。
  • shortcutkeyオブジェクトにaddCommandでkeyによって実行されるコマンドを作る。その後shortcutkeyオブジェクトにaddEevntlistenerでキーが押されるのを監視させる。

説明下手なので何言ってるかあんまりわかんないと思います。そこでMinibuffer 2007.11.15 の変更点 | 3.14に書かれている例を実際にGreasemonkey Scriptに起こして書いておきます。

せっかくなので先ほどのものを拡張します。

// ==UserScript==
// @name           Sample3 :Show Message by keypress
// @namespace      http://d.hatena.ne.jp/Constellation/
// @include        http://search.yahoo.co.jp/search?*
// ==/UserScript==

// Minibufferが動いているかどうかチェック。なければreturnでScriptを中止させている。
if(!window.Minibuffer) return;

window.Minibuffer.addCommand({
// 追加するコマンドの名前
name: 'Show::location',
// 追加するコマンド
command: function(){ 
window.Minibuffer.status('Sample','Google?...');
setTimeout(function(){window.Minibuffer.status('Sample','Google?...Yahoo!',150);},1000);
},
});

// ここまででコマンドを登録し終わった。
// ここからこのコマンドをScript内で呼び出す。

window.Minibuffer.addShortcutkey({
key: 'g',
description: 'Show::location::by::key', // 説明
command: function(){ // 実行するコマンド
window.Minibuffer.execute('Show::location');
window.Minibuffer.message('Yahoo!', 1000);
}
});

window.Minibuffer.addShortcutKeyは引数を3つとります。

  • key ショートカットキー 2ストロークキーは半角スペースで区切るようです。keypressイベントで見ているようなので、Shift+gのときは、"G"というように結果的入力される文字を入れればいいようです。
  • description : 説明というか名前です。MinibufferのHELPで表示されます。
  • command : コマンド自身です。

追記
Miniufferの30行目、keyCodeStrオブジェクトの中身を見てもらうと、特定キーには文字列が渡されていることがわかります。見てもらったほうが確実かもしれませんが、せっかくなのでここに付記します。

キーコード key 説明
8 'BAC' バックスペースキー
9 'TAB' タブキー
10 'RET' リターンキー(エンターキー)
13 'RET' リターンキー(エンターキー)
27 'ESC' エスケープキー
33 'PageUp' PageUpキー
34 'PageDown' PageDownキー
35 'End' Endキー
36 'Home' Homeキー
37 'Left' Leftキー(矢印左)
38 'Up' Upキー(矢印上)
39 'Right' Rightキー(矢印右)
40 'Down' Downキー(矢印下)
45 'Insert' Insertキー
46 'Delete' Deleteキー
112 'F1' F1キー
113 'F2' F2キー
114 'F3' F3キー
115 'F4' F4キー
116 'F5' F5キー
117 'F6' F6キー
118 'F7' F7キー
119 'F8' F8キー
120 'F9' F9キー
121 'F10' F10キー
122 'F11' F11キー
123 'F12' F12キー
32 'SPC' スペースキー

つまり、リターンキーにショートカットを加えたければ、

key:'RET',

とすればいいのです。
追記終わり

なかで使っているwindow.Minibuffer.executeはScript内で登録したコマンドを呼び出します。
この場合先に'Show::location'コマンドを登録しておいて、それをgキーを押したときに呼び出しています。
また、window.Minibuffer.execute('foo', stdin);
とすればコマンドfooにstdinを渡すことができます。

もうひとつ、window.Minibuffer.messageというものがあります。これはもう見てもらったらわかるとおりmessageなのですが、後ろの数字は何秒で消えるかです。省略時には400になります。
window.Minibuffer.message('Yahoo!');

Scriptを実行して、gキーを押してください。先ほどの意味のわからないメッセージが表示されると思います。でも自分はYahoo!よりはGoogleのほうが好きです。
これで一応ショートカットキー登録などまでできることとなります。後はpinされたノードなどをどうやって受け取るかがわかれば完璧です(と、思う)

その前にもう一方の登録方法を

// ==UserScript==
// @name           Sample3 :Show Message by keypress
// @namespace      http://d.hatena.ne.jp/Constellation/
// @include        http://search.yahoo.co.jp/search?*
// ==/UserScript==

// Minibufferが動いているかどうかチェック。なければreturnでScriptを中止させている。
if(!window.Minibuffer) return;

window.Minibuffer.addCommand({
// 追加するコマンドの名前
name: 'Show::location',
// 追加するコマンド
command: function(){ 
window.Minibuffer.status('Sample','Google?...');
setTimeout(function(){window.Minibuffer.status('Sample','Google?...Yahoo!',150);},1000);
},
});

// ここまででコマンドを登録し終わった。
// ここからこのコマンドをScript内で呼び出す。

//Shortcutkeyオブジェクトを手に入れる。
var shortcutkey = window.Minibuffer.getShortcutKey();
//ShortcutkeyオブジェクトにコマンドとaddEventListenerでの監視を追加する。

shortcutkey.addCommand({
key: 'g', // gキーでのコマンドを登録
command : function(aEvent){
window.Minibuffer.execute('Show::location');
window.Minibuffer.message('Yahoo!', 1000);
}
});

//addEventListener(監視対象,監視イベント,イベント伝播のどの段階でキャプチャするか)
shortcutkey.addEventListener(window,'keypress',true);

イベント伝播についてはJavaScript addEventListener() - とみぞーノートとか見てもらうとわかると思います。わからなかったらとりあえず今はtrueかfalseか入れておいたらいいと思います。よっぽどシビアなとき、ほかの登録と競合するときぐらいにしか問題にならないと思うのですがどうでしょうか。
まあこんな感じです。

Minibufferで基本的なものを作ってみよう。

さっきまでは実用性皆無のものをつくってました。そこで少しは実用性のあるものを解説します。
とりあえず拙作のDeleteCommandを適当に解説します。ほかのMinibuffer用Scriptよりレベルが低いので。
とりあえずコメントとして書き込みました。わかりにくいかも。

// ==UserScript==
// @name           DeleteCommand
// @namespace      http://d.hatena.ne.jp/Constellation/
// @description    delete tumblr on Minibuffer
// @include        http://www.tumblr.com/dashboard*
// @include        http://www.tumblr.com/show*
// @version        0.0.1
// ==/UserScript==

if(!window.Minibuffer) return; //Minibufferの存在確認。もはやお約束。
var $X = window.Minibuffer.$X; //$X関数を参照。でも参照しときながら使わなかった。

// Tumblr::Deleteのコマンドを登録
window.Minibuffer.addCommand({
  name : 'Tumblr::Delete',
  command : function(stdin){ //渡されてくるものをstdinとして受け取る。あとでpinされたnodeのnodelistを投げるつもり。
  stdin.forEach(function(obj){ //forEachでlistそれぞれのnodeに実行。objでそれぞれのnodeを受け取ってます。
    if (obj.className.match(/is_mine/)){ // 渡されたnodeのクラスにis_mineがはいっているか確かめている。入っていなかったら他人のエントリなので{}の中身を実行しない。今考えるとindexOfのほうが効率がよかった。
      var id = obj.id.match(/post([\d]+)/)[1]; // Deleteのための必須情報、Tumblrのエントリのidを手に入れる。
      window.Minibuffer.status('DeleteCommand'+id, 'Delete...'); //status情報表示。上でやったやつです。

      var data = encodeURIComponent('id') + '=' + encodeURIComponent(id); //手に入れたidからdeleteするためにpostするときのdataを作成。
      //GM_xmlhttpRequestに投げるオブジェクト。
      //自分はGM_xmlhttpRequest内に直接書くのが嫌なのでいつもopt作ってから投げてます。
      //optの中身の諸情報についてはGM_xmlhttpRequestの説明を探してください。
      var opt = {
        method: 'POST',
        url: 'http://www.tumblr.com/delete',
        headers : {
        'Content-Type' : 'application/x-www-form-urlencoded'
          },
        data: data,
        //onload内にはRequestが帰ってきてからの処理が書いてあります。
        //逆に言えばここの中身が実行されるのは、POSTが成功し、終了したときです。
        //引数のresはGM_xmlhttpRequestのresponseオブジェクトです。
        //POSTの場合なのでこの場合はあまり使いませんが。
        onload: function(res){
        window.Minibuffer.status('DeleteCommand'+id, 'Delete... done.', 100); //さっきのstatusを更新し、Deleteが成功したことを明示します。
        },
      }
      GM_xmlhttpRequest(opt); //GM_xmlhttpRequestを実行しています。
    }
  });
  },
});

//以下がショートカットキー登録です。
window.Minibuffer.addShortcutkey({
    //Shift+Dにショートカットキーを設定。
  key: 'D',
  description: 'Tumblr::Delete', // 説明文
  command: function(){
    var stdin = []; //nodelistがなかったときに備えてからの配列を宣言。
    try{
    //LDRizeに標準で入っているpinned-or-current-nodeコマンドを実行
    //このコマンドは、pinしているものがあったらそのnodelistを、
    //なかったら今さしているnodeをlistとして返します。
    //何にもなかった場合に備えてtryで実行
    //そしてそのnodelistをstdinで参照しています。
    stdin = window.Minibuffer.execute('pinned-or-current-node');
    } catch (e){}
    //Tumblr::Deleteコマンドを実行。
    //nodelistのstdinを渡しています。
    window.Minibuffer.execute('Tumblr::Delete', stdin);
    //終わった後Minibuffer標準搭載のclear-pinコマンドを実行し、pinをclearしています。
    window.Minibuffer.execute('clear-pin');
  }
  });

//debug用
function log (e){
  GM_log(e)
}

これをwindow.addEventListener('keypress', deleteTumblr, true);なんかで登録していくと気の遠くなるほどたいへんですが、Minibufferを利用するとこんなにあっさりかけます。

ほかにも

Minibufferの搭載している補完機能などをほかのScriptから使うことができます。ただ、自分はまだそれを使ってないので自信がないので、使ったら追記します。
もっといろいろ機能はありそうなので、見つけたら追記します。

感想

ある程度伝えられたかな?説明下手だから厳しい。。。
これかいてる途中に急に自分もまた作ってみたくなったのですが、それは別のエントリに分けます。