LDRizeの(一部だった)minibuffer用にシェルを組んでみた

あっというまにLDRizeが新しくなって、それもLDRizeのminibufferにコマンド追加してみたこと雑感で書いたことまでとりいれていただいて感謝です。

それにちゃんと追いつけてないながら XUL Apps > Tips > 選択範囲のリンクを収集する 〜 DOM2 RangeのcompareBoundaryPointsの使い方 – outsider reflex を見て、これをLDRizeでやりたい!と思っていたのでminibufferにかんたんなシェルを作ってパイプで繋げてあそべるコマンドを作りました。

minibufferが分離されて単独で動くようになったのは知っていたのですが、間抜けなことにいちどALt-xを押してみてあれなんか動かない(新しくしてなかったのかも)、と思ってからとりあえずいいやと思って8.24版のLDRize.addCommandのアプローチで実装しちゃいました。

デモ: twitterで囲んだ範囲のstatusページだけを開く

minibufferがブラウザ上で動くというのの利点は、マウスでちょこちょこっと操作するかんたんさと、細かく複雑な指定ができるコマンドラインがいっしょになっているところだと思います。囲んだ範囲のリンクをみんな開くみたいなのはブックマークレットでふつうにできて芸がないので、twitterのページでマウスで囲んだ部分のうち、発言のperlamlinkになっているリンクだけを開く、というのをやってみました。

まずはマウスで好きなだけ囲みます。

こんなかんじ。

そして : を押してminibufferを開きます。

スクリーンショットをとったときには selected, links, grep, openintab の4つのコマンドを作ってました。下の方にリンクしてあるスクリプトはさらにimages, xpath, echoを追加してあります。

シェルのコマンドラインと同じかんじでコマンドを入れてパイプで繋いでいきます。

Command Piped

selected は、マウスで選択されている範囲のノードを返すコマンドです。そのあとにパイプで繋がれているlinksコマンドは入力されたノードの中に含まれるAタグだけを取り出して返すコマンドです。
あ、ちなみにまだてきとう実装なのでselected以降のコマンドはLDRizeのコマンド補完機能は効きません。
そのあとにあるgrepコマンドは渡されたノードのリンクに引数で指定された正規表現が含まれているものだけを返します。今回はtwitterの発言permalinkだけを取り出したいのでpermalinkだけに含まれているstatusesを引数に渡しています。
最後のopenintabコマンドは、渡されたリンクをぜんぶ新しいタブで開きます。

タブがたくさん開いたところです。

わかりにくいのでヒストリで見てみます。

はじめにマウスで囲んだ範囲に含まれていた6つのpermalinkが開いているのがわかります。

今回はAタグだけを取り出すlinksを間に入れましたが、IMGタグだけを取り出すimagesを間に入れてもいいですし、取り出すものをxpathコマンドでXPathを使って指定することもできます。
間に入れるフィルタが作れるだけでなく、minibufferのコマンドはjavascriptで好きに書けるので、最後に画像をreblogしたりブックマークしたりjavascriptでできることなら何でも可能で、しかもちょっと動作をかえたいときにはコマンドにちょっと引数を渡せるので細かい用途ごとにべつべつのブックマークレットを用意しないといけないみたいなのもなくなります。minibuffer最高!

スクリプト

というわけでよかったらためしてみてください。

実装アプローチ

ldrize.user.js, minibuffer.user.js本体に少し手を入れないと実現できなかったのですが、とりあえずLDRizeのほかの部分と整合性がとれているか何も考えずに手を入れました。

まずminibuffer.user.jsの、minibufferでEnterを押したときに呼ばれるbindDoAndExitをいじりました。minibufferからコマンドラインに入れられた文字列を作った Minibuffer.__Shell.parse() に渡してパース、実行するべきコマンドのリストを得てます。

コマンドを実行するときに、コマンドの引数と、シェルでいう表示ユン入力を渡したいので引数を二つ増やしてます。

@@ -174,11 +195,18 @@
      Minibuffer.updateComplationList();
   },
   bindDoAndExit : function(){
-     var cmd = Minibuffer.getCandidateCommandHash()[Minibuffer.input.innerHTML];
-     if(!cmd) return;
-     var str = Minibuffer.input.innerHTML;
+     var commands = this.__Shell.parse(Minibuffer.input.innerHTML);
+     //FIXME: check command existence first, and  exit and execute them all.
      Minibuffer.exit();
-     cmd(str);
+     var stdin = null;
+     var ret = commands.forEach( function ( command ) {
+       var cmd = Minibuffer.getCandidateCommandHash()[ command.name ];
+       if(!cmd) {
+           return null;
+       } else {
+           stdin = cmd(command.name, command.args, stdin);
+       }
+     } );

minibufferでcmdとして呼ばれた関数はLDRize.minibufferCallbackなので、こっちも引数を増やします。stdinが空のときはLDRize側でセットされているピンのリストを渡します。

コマンドが返す値はtrue/falseでLDRizeでセットしたピンをクリアするかそのままかの指定に使われていますが、これをそのまま流用してコマンドのフィルタ結果を返すようにして、さらにreturnを追加してminibuffer側に結果を戻します。

こうすると本来のピンを外す、外さない機能と鑑賞しそうですが、ふつうminibufferの最後に実行するコマンドはopenintabのようなフィルタ結果を返さないコマンドなので、それがピンを外すか外さないかの本来のLDRizeのコマンドとしての値を返してあげればだいたい問題ない気もします。

@@ -981,14 +981,17 @@
      for (x in LDRize.command) obj[x] = LDRize.minibufferCallback;
      window.Minibuffer.complete(obj, LDRize.minibufferCallbackExit, LDRize.PROMPT);
   },
-  minibufferCallback : function(string) {
+  minibufferCallback : function(string, args, obj) {
      var lst = LDRize.getPinnedItemsOrCurrentItem();
-     var obj = {
+     obj = obj || {
        paragraphes : lst,
        links: lst.map(LDRize.getLinkURL),
      }
+       obj.args = args;
+
      var continuePinList = LDRize.command[string](obj); // do command
      if(!continuePinList) LDRize.clearPinlist();
+     return continuePinList;
   },
   minibufferCallbackExit : function(){
      document.addEventListener('keypress', LDRize.handleKey, true);

ちなみに、一番はじめに書いたコマンドには入力としてピンを立てたノードのリストがくるので、

images | echo

とすると、ピンを立てたパラグラフの中に含まれる画像のノードとURLがFirebugのコンソールに出力されます。

思ったこと

minibufferすごすぎると感動しながらシェルをつくって思ったことをいくつか書きます。

JSONストリームのエディタ

LDRizeはminibufferのコマンドを呼び出すときに、関数の引数として

{
    paragraphes: array_of_domNodes,
    links: array_of_strings_contain_url
}

という、DOMのノードのリストとそれに結びつけられたURLのリストを渡してきます。
linksgrepのようなコマンドは、まさに かんたんすぎ かっこよすぎ Yahoo pipes にちょっと書いてた IBM dW : XML : XMLの論考: マイクロフォーマットのパイプストリーム – Japan と同じな感じがしました。minibufferはブラウザ上でLDRizeのピンのようなKUI(Keyboard User Interface)とGUI(マウス)で抽出したデータをJSONで処理するかんじです。

JSONを処理するパイプ、という気持ちでコマンドを書いていていまのLDRizeの仕様で気になったのが、コマンドに渡されるデータがオブジェクトのプロパティにArrayが入っているところでした。JSONデータを加工するパイプとしてコマンドを捉えると、プロパティのはいっているオブジェクトのArrayが渡されるほうが便利でした。

前者だとgrepの実装がこう

        name: 'grep',
        command: function (obj) {
            var regex = obj.args.shift();
            var modifier = obj.args.shift();
            var re = new RegExp(regex, modifier);
            var paragraphes = [];
            var links = [];
            for ( var i = 0; i < obj.links.length; i++ ) {
                var u = obj.links[i];
                if ( u.match( re ) ) {
                    paragraphes.push( obj.paragraphes[i]);
                    links.push(u);
                }
            }
            obj.paragraphes = paragraphes;
            obj.links = links;
            return obj;
        },

なってforでループさせて再度Arrayをつくるというのをせざるをえないですが、データが後者のようにオブジェクトのArrayになっていると

        name: 'grep',
        command: function (obj) {
            var regex = obj.args.shift();
            var modifier = obj.args.shift();
            var re = new RegExp(regex, modifier);
            return obj.filter( function ( item ) {
                return item.link.match( re );
            }
        },

でできるのですごく自分がかしこくなった気分になれそうです。LDRizeのピンを設定する側の処理のところはコードをよく見ていないのでそっち側で何か問題があったりするかもしれないですが、こうしていただけるとコマンドを作るときにはすごくかっこよく書けるようになりそうですー。

コマンドでのプロパティ追加

LDRizeからピンのデータとしてコマンドに渡されるデータはノードとそれに対応するURLだけど、途中のフィルタで

{
    paragraph: domNode,
    links: url,
    name: ....,
    reblog_id: ....,
}

みたいに新しくプロパティを追加したオブジェクトを結果として返すと、それより後ろのコマンドでそのプロパティを参照できる、というようにしておくと自由度が広がるのかも。自分が書いたものはそうなるようにしてみました。特に何にも使ってないですけど。

chrome特権

やっぱり最後に saveto localdisk とか saveto flickr とか reblog とかしたくなるのでminibufferだけextensionにしてコマンドでディスクに書いたりしたいです!minibufferがLDRizeと別で単体でも動くように変わったおかげでちょっといじればできそうな気がするのでこれはそのうちやってみようとおもいます。
特権が必要でかつ必要そうなのはいまのところsaveto localdiskくらいしかおもいつかないし、ほかはGreasemonkeyでがんばればできるので、それだけのためにセキュリティリスクとインストール後に再起動しないといけないお手軽でなさ(Fx3で再起動がいらなくなるみたいな記述があったけどどうなんでしょう)を呼び込む価値があるのか微妙ですが….

感謝!

こんなにたのしいぴかぴかのおもちゃを作っていただいてありがとうございます。前回は出過ぎたことを書いたと反省しつつも、このポストがなにかしら今後の設計の参考のお役に立てればと思って今回も書いてみました。


About this entry