Category Archives: LDRize

LDRize

AutoPagerizeのスクリプト実行順序制約をなくせるようになりました

Tumblrが新しくなって、よく見ていた/show/quotes/by/everyoneがちゃんとページングされなくなって悲しいと思っていたらcxxさんがFix Tumblr Dashboard Pagination for Greasemonkeyというスクリプトを書いてくれていました。

しかし21世紀はじめの10年最後の2009年ももう終わろうとしているにも関わらず、未だにTumblr dashboard reblog 4点セットのAutoPagerizeLDRizeMiniBufferreblogCommandの実行される順序をちゃんと覚えておかないといけないなんてローテクすぎる!という怒りにまかせて、順番に関係なく入れておけば動くように細工をしました。

それぞれ上記以降のバージョンであれば、Greasemonkeyで実行される時の順番を気にせず、とにかくインストールしておけばよくなりました。

しくみ

AutoPagerizeが実行されたときに

	var ev = document.createEvent('Events')
	ev.initEvent('GM_AutoPagerizeLoaded', false, true)
	window.dispatchEvent(ev)

というコードが実行されてGM_AutoPagerizeLoadedという名前のイベントが送られてくるようにしました。LDRizeのようにAutoPagerizeに依存してなにかを実行したときに、もしwindow.AutoPagerizeが存在しなかったらGM_AutoPagerizeLoadedイベントが送られてくるまで待ってから実行するようなコードを書けば、スクリプトが実行される順序に関係なく動作させることができるようになります。

以下はAutoPagerizeでページが継ぎ足されたときに、継ぎ足された部分をLDRizeが正しく認識するためのコードです。実行したいコードをaddFilterHandlerという名前の関数にしておいて、window.AutoPagerizeが存在していればそのまま実行、存在していない時はAutoPagerizeからGM_AutoPagerizeLoadedイベントが送られてくるのを待ってから実行することで、AutoPagerizeに依存するスクリプトを順序に関係なく正しく動作させることができます。

      var addFilterHandler = function(){
          window.AutoPagerize.addFilter(
              function(pages){
                  self.removeSpace();
                  setTimeout(function(){
                      self.initParagraph(pages);
                  }, 0);
              });
      }
      if(window.AutoPagerize){
        addFilterHandler();
      }else{
        window.addEventListener('GM_AutoPagerizeLoaded', addFilterHandler, false);
      }

AutoPagerizeと連動するスクリプトを書く人へのおねがい

AutoPagerizeに依存する部分を

      var f = function () {
        // やりたいこと
      }
      if(window.AutoPagerize){
        f();
      }else{
        window.addEventListener('GM_AutoPagerizeLoaded', f, false);
      }

と書くとスクリプトが実行される順序を気にしなくてよくなって、使ってくれる人の手間も省けるのでぜひご採用ください。

Webサイト側でのAutoPagerize検出

うれしい副作用として、ユーザスクリプトと同じようにHTMLドキュメント側でもGM_AutoPagerizeLoadedをlistenしておくことでAutoPagerizeが存在するかどうかを検出することができます。

  var isAutoPagerizePresent = false;
  window.addEventListener( 'load', function () {
    alert(isAutoPagerizePresent);
  }, false );
  window.addEventListener( 'GM_AutoPagerizeLoaded', function () {
    isAutoPagerizePresent = true;
  }, false );

Firefox3.1beta2+OSXの場合、GM_AutoPagerizeLoadedイベントが発生するタイミングはjQueryのreadyメソッドよりも後、loadイベントよりも前でした。

オチ

もとのcxxさんがFix Tumblr Dashboard Pagination for Greasemonkeyの説明をよく読むと

AutoPagerizeと併用する場合は、「ユーザスクリプトの管理」でAutoPagerizeよりもに置いておく必要があります。

と書かれていたのでした… にしないといけない制約しか考慮していなかったのでにしないといけない場合は、今後も順番を気にし続けるか、AutoPagerizeにそういう細工をして取り込んでくれるようにとswdyhにpull requestを送ってください。

Flickrの写真をpostする LDRize Minibuffer flickr.share コマンド

FlickrでLDRizeでピンを立てたものをTumblrにpostするLDRizeのMinibuffer用コマンドです。

ダウンロード

LDRize Mibuffer flickr.share command – Userscripts.orgからどうぞ。

使い方

reblog commandと同じです。Tumblrにpostしたい写真をpでピンを立てていって気が向いたときに

pinned-node | flickr.share

でピンを立てた写真をtumblrにpostすることができます。

ただ、Flickrの仕様上3回リクエストを出さないとtumblrにpostできないためreblog commandに比べて完了までに時間がかかります(5秒くらい?)。焦らずお待ちください。

LDRizeが効くページでは使えると思います。ピンを立てたparagraphの中で一番大きい写真がpostされるようになっています。うまくいかないところがあったら教えてくださいー。

注意点

tumblrにログインしていない、枚数制限を超えているなどの理由でpostに失敗したときでもエラーが出たりしないで成功したことになるのでご注意ください。
Tomblooで使われているnsIXMLHttpRequestのGreasemonkeyバージョンGM_xmlhttpRequestが機能的に貧弱でリダイレクトされてもわからないためです…

感想

LDRize mibuffer tumblr reblog commandのほうの中身はほとんどShareOnTumblrで出来ているのですが、そのShareOnTumblrはTomblooに吸収されてなくなったのでreblog commandもいいかげんtomblooベースにしないと(コアの部分を自分でメンテナンスしないといけなくなって)めんどくさくなるなーと思いつつ、動くからいいやと思っていました。

FlickrでもtumblrのdashboardみたいにLDRizeでピンを立てていってpostしたいなーと思って、でもpostする部分は既にTomblooで実現されている機能なのでTomblooからちぎって繋げばいける、というわけで行数にして95%をTombloo0.0.9から持ってきています(そのうちの85%はMochiKitのコード)。のこりの5%のうち2%は
Curiosity is bliss: XMLHttpRequest – Security Bypassで自分で書いたのは250行くらい。

ShareOnTumblrのコードは流用しにくかったけれど、TomblooはTombloo、0.0.7 – 実用に15日書けてリファクリタングしたと書かれている通り、サービスの抽象化のしかたも(いろんなサービスからデータを読み出して、いろんなサービス(TumblrとFFFFOUND!のふたつ)に書き込むことができます)、非同期処理のハンドリングもほんとうに素晴らしくて、わずかに1行書き換えるだけでそのまま再利用できました。

flickr.share commandの仕事

Tomblooはpostしたいものをマウスを使って(つまりGUIで)選びます。postするものは、基本的にはマウスでクリックされたHTMLの要素(画像とか選択されたテキスト)になります。LDRizeの場合は、ピンを立てるのはページの中にある繰り返し要素のひとつひとつ(paragraph)なので、そのparagraphの中にある何をpostするのかまではわかりません。

flickr.shareは、LDRizeから渡されたparagraphから、一番大きなimg要素を取り出してcontextの中に設定して

Tombloo.Service.share(context, Tombloo.Service.extracters[ 'Photo - Flickr' ]);

を呼び出しているだけです。

FFFFOUND!でもLDRizeでピンを立ててtumblrにpostしたいと思えば'Photo - Flickr'の部分を'Photo - FFFFOUND!'に変えればffffound.shareコマンドができちゃう! わけです。

Tomblooの中には既に31個くらいのデータの読み出し元(と、読み出すときに特定の手続きが必要だったりすることもあるのでその読み出しかた)が定義されているので、こうやってちまちまuser.jsを書くんじゃなくてTomblooの側からGreasemonkeyのsandboxにアクセスする方法でもってMinibufferにコマンドを追加する方向でいこうと思います。

Thanks

LDRize Minibuffer flickr.share command contains following codes. Thanks for the respective developers.

paragraphのdt/dd問題

AutoPagerizeのSITEINFOについての考察の複数の要素でひとつのparagraphが構成されている場合の補足。

いくつかのサイトの構造を見ているうちにMozilla Developer Centerの検索結果ページのように、ひとつのparagraphがdtとddで構成されている場合があるのに気がついた。

この書き方はひとつのエレメントがひとつのparagraphであってほしい立場からすると美しくないけれど、この書き方は仕様的にLists in HTML documents (ja)正しい書き方だ。ひとつのエレメントがひとつのparagraphであることが望ましいという主張は、DL要素の仕様と相容れないことになる。

親要素が何かをチェックして、dlの場合はdtから次のdtまでがひとつの繰り返しだと判断するのが妥当ということになる。

chrome特権つきのLDRize Minibufferコマンドを作る

2007.11.13 追記

すいませんこれちょっとうまくいかないかもです。
non-privilegedの関数からだとprivilegedで定義された関数のなかでも制約があるみたいです。

2007.11.27 追記

とりあえずGreasemonkeyに特権関数を追加するprivileged monkeyになりました。

やっとGreasemonkeyスクリプトのsandboxオブジェクトにFirefox extensionから簡単にアクセスする方法をひねり出した。まだ何も実装していないけれど、これでMinibufferから

pinned-node | images | save-as localdisk

で、ピンをつけたparagraphの中にある画像をローカルに保存する、とか

pinned-node | images | save-as flickr

それをFlickrにアップロードするみたいなコマンドを実装することができるようになる。

Greasemonkeyスクリプトのsandbox

Greasemonkeyはgreasemonkey.jsの中にあるgreasemonkeyServiceのメソッドinjectScriptsでGreasemonkeyスクリプトのグローバルスコープになるsandboxを作って全てのスクリプトを実行している。

  injectScripts: function(scripts, url, unsafeContentWin, chromeWin) {
    var sandbox;

... snip ...
    for (var i = 0; script = scripts[i]; i++) {
      sandbox = new Components.utils.Sandbox(safeWin);

... snip ...
      sandbox.window = safeWin;
      sandbox.document = sandbox.window.document;
      sandbox.unsafeWindow = unsafeContentWin;

... snip ...
      this.evalInSandbox("(function(){\n" +
                         getContents(getScriptFileURI(script.filename)) +
                         "\n})()",
                         url,
                         sandbox,
                         script);
    }
  },

Minibufferはsandbox.window.Minibufferを介して他のスクリプトからコマンドを追加することができるようになっているけれど、そのsandboxはローカル変数になっていてinjectScriptsの実行が終わるとgreasemonkeyServiceのインスタンスから参照できなくなるのでなんとかしてsandboxをゲットする必要がある。

さいわいgreasemonkeyServiceというXPCOMコンポーネントのインスタンスは、拡張機能の名前空間からGreasemonkey本体が使うGM_BrowserUIという名前経由でGM_BrowserUI.gmSvcで参照できる。greasemonkeyServiceはXPCOMコンポーネントなため、IDLで記述されたメソッドしかアクセスできない。と思ってたんだけどjsで実装されているXPCOMコンポーネントならば実はJavaScript XPCOM コンポーネントの状況に書いてあるwrappedJSObjectを通して全部のメソッドにアクセスできるのでした。

まれに、JS コードの呼び出しから、実装している JS コンポーネントの JSObjectに実際にアクセスする必要がある可能性があります。このため、 xpcom コンポーネントの回りの xpconnect ラッパーは、現在 wrappedJSObject というプロパティをサポートしています。

と書かれております。好き勝手にいじり回せて便利ですね。

というわけでinjectScriptsのコードを無理矢理書き換えてevalすれば特権つきのMinibufferコマンドを作れそうです。

                    var wo = GM_BrowserUI.gmSvc.wrappedJSObject;
                    var code = wo.injectScripts.toSource();
                    code = code.replace(/}\)$/, '\
                        var tab = chromeWin.top.document.getElementById("content"); \
                        var browser = tab.getBrowserForDocument(sandbox.window.document); \
                        chromeWin.top.MinibufferPrivilegedCommands.onGMScriptsExecuted(sandbox); \
                    })' );
                    wo.injectScripts = eval(code, wo);

セキュリティ

sandboxは全てのGreasemonkeyスクリプトで共有されているので、特権をつけたいGreasemonkeyスクリプト以外からも特権つきのコマンドを参照できるようになってしまいます。

任意の特権コードを実行できるようになるわけではないので(意識的に可能にするように書かなければsandboxから任意コードを特権付きで実行することはできないはず)、信頼してインストールしていることが前提にあるGreasemonkeyスクリプトから特権がなければ実現できない特定の機能を呼び出せてもいいかなあとも思いますが、なんとかしてできないようにするに越したことはないと思います。

どうやって実現するかは今後の課題で。

top.MinibufferPrivilegedCommands = {
    success: false,
    initailzed: false,

    init: function () {
        var retry = 30;
        var timerid = null;
        var interval = 1000;

        if ( this.initailzed )
            return;

        var self = this;
        this.timerid = window.setInterval( function () {
            var stop = ( GM_BrowserUI || retry < 0 );

            if ( GM_BrowserUI ) {
                var browser = GM_BrowserUI.tabBrowser.
                                getBrowserForDocument(window.content.document);
                if ( ! browser ) {
                    stop = true;
                } else if ( ! self.success ) {
                    var wo = GM_BrowserUI.gmSvc.wrappedJSObject;
                    var code = wo.injectScripts.toSource();
                    code = code.replace(/}\)$/, '\
                        var tab = chromeWin.top.document.getElementById("content"); \
                        var browser = tab.getBrowserForDocument(sandbox.window.document); \
                        chromeWin.top.MinibufferPrivilegedCommands.onGMScriptsExecuted(sandbox); \
                    })' );
                    wo.injectScripts = eval(code, wo);
                    self.success = true;
                }
            }

            if ( stop ) {
                window.clearInterval(timerid);
                timerid = null;
            }

        } , this.interval );
        this.initailzed = true;
    },
    onGMScriptsExecuted: function (sandbox) {
        sandbox.Minibuffer.addCommand( ...... );
    }
};

AutoPagerizeのSITEINFOについての考察

AutoPagerizeSITEINFOpageElementについて気がついたことを書いておく。

LDRize paragraph構造とplagger EntryFullText構造

AutoPagerizeで複数のページをひとつに繋ごうとするとき、各ページの構成には大きくわけて二種類ある。

ひとつめはGoogleの検索結果のように、ひとつのページに10回程度の繰り返し部分があるもの。この場合pageElementにはこの繰り返されている部分にマッチするXPathが書かれている。これはLDRizeのSITEINFOにおけるparagraphで表現されるものと一致するので、ひとつのページ内に繰り返される部分が存在し、その部分ひとつひとつにマッチするXPathをpageElementとして記述するものをLDRize paragraph構造と呼ぶことにする。

もうひとつはCNETのインタービュー記事のように、多くの場合ひとつのページの中にはひとつの長い文章が記述されていて繰り返し部分が存在しないもの。この場合pageElementにはそのページにある本文を構成する文章全体にマッチするXPathが記述される。plaggerのFilter::EntryFullTextで取り出そうとする部分に近いので、ページの本文を構成する文章全体にマッチするXPathをpageElementとして記述するものをplagger EntryFullText構造と呼ぶことにする。

美しくない現実世界の問題

基本的には前述の通りページの構造は二つに分類することが可能で、それぞれにあった方法でpageElementを記述することになる。しかし、実際のウェブページには論理構造とマークアップが一致してないことに起因する美しくなさが多数存在するため、結果としてpageElementのXPathの記述も美しくなくなる。

複数の要素でひとつのparagraphが構成されている

XPathの記述的にも論理的にも、ひとつのparagraphがHTML上でひとつの要素として記述されていることが望まれるが、現実に行われているマークアップは繰り返しの単位がひとつの要素であるように書かれていないことがある。HTML上の複数の要素でひとつのparagraphが構成されているとXPath単体ではどこからどこまでがひとつのparagraphなのかを表現することはできない。

実例としてTechCrunch Japaneseがあげられる。TechCrunch Japaenseは、ひとつのページに5つのエントリ全文が一覧表示されるようになっている。各エントリは日付を表すh2タグと本文を持つdivの二つの要素で構成されている。

この場合XPathではどの部分がひとつのparagraphかを表現することができない。

id('content')/*[ self::h2 or self::div[@class='post']]

と書けば、全てのparagraphに含まれている要素集合と同じものにマッチさせることができるが、各パラグラフがどの要素で構成されているのかという情報は失われている。(XPathで得られた要素集合の並びを調べて、ひとつのparagraphを推定するのは簡単かもしれない)

本文のテキスト要素とその他の部分が同一要素のsiblingになっている

この問題はCSSが一般的に使われるようになる前の古い時代に作られたサイトでよく見られる。文章のタイトルと本文とがひとつのIMG要素やHR要素で区切られていると、XPathで本文だけを切り出すことは非常に困難である。

実例として東京ふーどページの新店情報があげられる。
新店情報はdiv.eventtextの中に全て入っており、各繰り返し部分のタイトルはdiv.eventtitleの中に入っているが、本文は全てdiv.eventtextの子要素のテキストノードとして入っている。HR要素が各繰り返しの区切りとして使われている。

一見following-siblingを用いて記述することだけはできそうだが、よく見るとdiv.smallphotonewが存在するときと存在しないときがあるため

//div[@class="smallphotonew"]/following-sibling::text()

という記述では漏れが出てしまう。努力すればすべてを網羅する記述はできそうだが、XPathに習熟していなければ書くことはできないだろう(自分にはできないです)。

この問題は、もともとの文章がplain textでそれをHTMLに流し込んだものでも発生する。こちら第2編集部隊から本文だけをXPathで切り出すことはできない。(そもそも全文がひとつの要素の子要素のテキストノードになっているから)

まとめ

AutoPagerizeのpageElementには、LDRize paragraph構造を表現するものとplagger EntryFullText構造を表現するものとがあると思ったけれど、実際に調べてみるとHTMLの構造が原因でparagraphをXPathで正確に表現できないことも多く、LDRize paragraph構造を持っているページでもplagger EntryFullText構造でpageElementが記述されているものも多い。

繰り返し部分を繰り返しとして記述すればよりAutoPagerizeのpageElementとLDRizeのparagraphの互換性が高くなるが、実際に書かれているHTMLの美しくなさがそれを阻んでいる。

新しくなったMinibufferの感想

一日遅れで LDRize minibuffer reblog command を作ってみた感想をこちらに。

まずはじめにLDRizeの(一部だった)minibuffer用にシェルを組んでみたを書いてからほんの数日で、自分がちょっと時間かかりそうだなーとあきらめた、ちゃんとしたコマンドの補完機能だけでなく、エイリアスにヒストリまで実装されていて(Ctrl-Rで検索可能だし)、そしてコードもきれいで圧倒されました。

感想と言っても細かいことばかりなのですが、いくつか。

SITEINFOを参照したい

いまのreblogコマンドは、ピンの立てられたパラグラフからXPathでreblog用のリンクを取り出しています。できれば、このXPathもSTEINFOに書いてしまってコマンド側からはそれを参照して使うようになっているほうがメリットがあるのではと思います。

いまのreblogコマンドはreblogコマンド自身の中にXPathを書いているのでtumblr dashboardのデザインが変わってXPathを変更しないといけなくなったときに

  1. 誰かが壊れているのに気がつく
  2. 誰かXPathが書ける人が修正する
  3. reblogコマンド修正版をアップロードする
  4. 使っている人それぞれがreblogコマンドをアップデートする

という過程を経て正常に機能するようになります。

SITEINFOを参照して動くようになっていれば

  1. 誰かが壊れているのに気がつく
  2. 誰かXPathが書ける人がSITEINFOを修正する
  3. 使っている人のSITEINFOキャッシュが切れて更新されたら正常に機能するようになる

というふうに、誰かが直せばみんながハッピー、という状況を作り出せるのでSITEINFOを参照できるようにしてほしいです!

デフォルトのstdin

はじめ毎回pinned-nodeと書いてパイプで繋げるのがめんどくさいので、MinibufferでもLDRizeのようにデフォルトでstdinにピンを立てているparagraphesが入ってきたらいいのに、と思ったのですが、エイリアスが作れるのに気がつきました。どっちでもいいと思いますがpinned-nodeが使われることが多いようだったらデフォルトにしてもいい気もします。

evalハック

ほかのスクリプトからLDRizeやMinibufferのインスタンス本体を直接は操作できないようにメソッドだけを渡しているのは、むやみにメソッドを公開しない設計なのだと思っているのですがいくつかどうしても入れたい部分があったので 実用 – Firefox、evalの第二引数、プライベートメンバ/クロージャーの実行コンテキストへのアクセス に書かれているevalで隠蔽されているインスタンスにアクセスして上書きしています。

ひとつだけ、ぜひ採用してほしいのがMinibufferをopen/closeしたときのイベントを

 var Minibuffer = {
+   listeners: [],
+   dispatchEvent: function ( event_name, data ) {
+     Minibuffer.listeners.forEach( function ( listener ) {
+            if (event_name in listener) {
+                try {
+                    listener[event_name].apply(listener, [data]);
+                } catch (e) {
+                   trace(e);
+                }
+            }
+     } );
+   },
+   removeEventListener: function (obj) {
+       this.listeners = Minibuffer.listeners.filter( function ( listener ) {
+           return ( obj != listener);
+       } );
+   },
+   addEventListener: function (obj) {
+       Minibuffer.listeners.push(obj);
+   },
@@ -153,6 +194,7 @@
   },
   complete: function(candidates, callbackExit, prompt) {
      Minibuffer.init();
+     Minibuffer.dispatchEvent("show_minibuffer", null);
      if(prompt) Minibuffer.htmlprompt.innerHTML = prompt;
      Minibuffer.callbackExit = callbackExit;
      Minibuffer.candidate_command_hash = candidates;
@@ -183,9 +225,11 @@
      document.body.removeChild(Minibuffer.container);
      document.removeEventListener('keypress', Minibuffer.handleKey, true);
      Minibuffer.callbackExit();
+     Minibuffer.dispatchEvent("hide_minibuffer");
   },
   KEYBIND : { 
      'C-m'  : 'bindDoAndExit',
      'RET'  : 'bindDoAndExit',

こんなかんじでlistenできたらと思っています。なんでそんなのが必要かというと、カーソルキーをMinibufferが開いていないときはj,kとして機能するようにして、Minibufferが開いているときには元に戻して↑↓でコマンドを選択できるようにしたいのですー。

あとはbugfix的なものだけ貼っておきます。
pinned-node | のようにパイプで繋いだあとコマンドを入力するとき↑↓を押すと、パイプより前のコマンドが消えちゃうので

   selectCandidate : function(oldNode, newNode){
-     Minibuffer.input.value = newNode.innerHTML;
+     var currentNode = Minibuffer.input.value.match( /.*?\|\s*/ ) || '';
+     Minibuffer.input.value = currentNode + newNode.innerHTML;
      newNode.setAttribute('class','gm_minibuffer_selected');
      if(oldNode) oldNode.removeAttribute('class',0);
   },

パイプより前の部分を入れるようにしました。

backspaceで入力したコマンドを削っていったときに補完候補が変化しなかったので

  bindDeleteForwardChar : function(){
      var begin = Minibuffer.input.selectionStart;
      var end   = Minibuffer.input.selectionEnd;
      var text  = '';
      var pos; 
      if(begin == end){
          text = Minibuffer.input.value.slice(0, begin) + Minibuffer.input.value.slice(end + 1);
      }else{ 
          text = Minibuffer.input.value.slice(0, begin) + Minibuffer.input.value.slice(end)
      }
      Minibuffer.input.value = text;
      Minibuffer.input.setSelectionRange(begin, begin);
-     if(Minibuffer.input.value == '') Minibuffer.updateComplationList();
+     Minibuffer.updateComplationList();
  },

ifを削って変化するようにしました。

LDRize minibuffer reblog command

補足 2008.6.20

現在は /lang/javascript/userscripts/reblogcommand.user.js – CodeRepos::Share – Trac でメンテナンスされています。

v3対応について

11.17くらいからtumblr側でのreferrerのチェックが無くなったようでRefControlは必要なくなりました。
tumblr v3になってreblog時にrefererをチェックするようになりました。そのためrefererを変更することができないGreasemonkeyスクリプトではなんともならないので、とりあえずRefControl :: Firefox Add-onsをご利用ください。インストール後ステータスバーに出てくるアイコンからAdd siteで

site: www.tumblr.com
Action: Forge

を追加すればokです。

ようやくLDRize version 2008.08.28で分離されたMinibufferに対応しました。

今回からダウンロードは LDRize mibuffer tumblr reblog command – Userscripts.org で。

Minibufferからの使い方

Minibufferから使うときは、ふつうにpでピンを立てたあと、Alt-xを押してMinibufferfを開いて

pinned-node | tumblr.reblog

でreblogできます。このときお好みに合わせて間にreverseを入れたりもできます。気分に合わせてどうぞ。

Minibufferのエイリアスを使う

で、ここで毎回 pinned-node | tumblr.reblog って入れるのとかありえない!ってなるわけですが、そういう人のためにMinibufferにはエイリアスが用意されています。pinned-node | tumblr.reblogと入れたあとEnterを押さないでCtrl-cを押します。するとダイアログが出てくるので

好きな名前をつけてコマンドを保存します。
そうすると次回からMinibufferにreblogがコマンドとして使えるようになって、長いコマンドを入れなくてすむようになります。

いちおうLDRizeからも使えます

エイリアスを設定するのもめんどくさい、というひとは、これまで同様:を押して出てくるLDRizeのほうのreblogコマンドを使うこともできます。

ただしreblogという名前でMinibufferのエイリアスを作っていると、たぶん名前がかぶってるのが原因でLDRizeのコマンドのreblogが動かなくなるので注意。個人的にはMinibufferでエイリアスを作ってreblogするのをおすすめします。

FLASH KEYライクフィードバック

まえのやつはコマンドを実行してもreblogされていのかされていないのかわからなくて、どうにかしたいなとおもっていたところに
実用 – Stroboの動画に出てくるピンクのFLASH KEYがかわいくてよかったのでFLAHS KEYをちぎってもってきてつかわせてもらっています。これでいまどれくらいreblogが進んでいるかわかるようになりました(ピンクじゃないですけど)。

はじめにどれだけreblogするかが表示されて

ピンを立てたpostをreblogし終わるたびにだれだけ終わったかがパーセンテージで出てきます。同時にセットされていたピンが外されて右下に出ているピンが設定されている数が減っていきます。

61

最後に100%になってreblog完了です。

スクリプトの並べかた

たんぶら部 – Tumblove – - MCSG SYM – Autopagerize+LDRize+Minibuffer+Reblog Command でLDRizeやAutoPagerizeやreblogコマンドスクリプトをインストールする順番が分からない、と書かれているのでこちらでも補足しておきます。reblogコマンドはMinibufferやLDRizeよりもあとに実行するようにしてください。
上のページでLDRizeのシロさんがコメントに書かれている通り、実行される順番はFirefoxのステータスバー右端にあるサルのアイコンをクリックしてManage User Scriptsで出てくるリストで変えることができます。

デフォルトでははじめにインストールされたものが先に実行されるようになっています。

Enjoy!

というわけでFLASH KEYのみためと、補完にヒストリ、エイリアスといったシェルらしい機能が備わったあたらしいMinibufferに(ようやく)対応したreblogコマンドで楽しいtumblrライフを!

LDRizeのminibuffer用reblogコマンド LDRize_tumblr_reblog.user.js 8.28版対応バージョン

LDRize version 2007.08.28で、コマンドに渡される引数が変更になったのでそれに対応して新版で動くようにしました。

ldrize_command_tumblr_reblog-0.0.2.user.js

前のバージョンの LDRize_tumbler_dashboard_reblog.user.js ではあらかじめコマンドを実行しておいたりする必要がありましたが、LDRizeが新しくなったのでその必要が無くなっています。また、前のバージョンはtumblr dashboardでしか使えませんでしたが、今回のやつは *.tumblr.com でも使えます。
さらに、なんか前のやつだとXPathが変になったりしてたのですが、ちょこっと使っただけなのであやしいですが問題が出なくなったかもしれません。LDRizeでキーボードだけでreblogできるようになるとマウスホイールとかもうぜんぜんだめなかんじになりります。すごい。

ふつうにpでピンを立てていって(さっきまでピンを立てたエントリをpostしたひとのアイコンが一覧で出てた気がするのですが、スクリーンショットではピンを立てた数だけ表示されてます。アイコン並んでるのかわいかったのでとりたかった)、最後に(もしくは次のページがロードされるのを待っている間に) : でminibufferを開いて

reblogでreblogされます。

いまのところはFirebugのコンソールでreblogされたのが確認できます。
LDRizeとおなじみためできれいに出したいです。

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で再起動がいらなくなるみたいな記述があったけどどうなんでしょう)を呼び込む価値があるのか微妙ですが….

感謝!

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

LDRizeのminibufferにコマンド追加してみたこと雑感

LDRize(2007.8.24版)のミニバッファにreblogコマンドを追加するスクリプト LDRize_tumbler_dashboard_reblog.user.js を作ってみて感じたことをべつでこっちに書きます。

とりあえず、ほんとに誰も見ていないはてなダイアリーのメモ書き程度な自分の日記にコメントしていただいてありがとうございました。びっくりました。AutoPagerizeと組み合わせたときにちょっと不便だったところのパッチを書いてお送りしようと思ってるのですが、とりあえずミニバッファ用のコマンドを作ってみていくつか気がついたことを書いておきます。

ピンはパラグラフに対してつけたい

現状(8/24バージョン)ではピンはlinkに対してつけるようになっていて tumblr dashboard のようにlinkがsiteinfoに定義されていないときにはピンを立てること自体ができないですが、ミニバッファのコマンドでparagraphもうけとれるようになったし、なにかとlinkがなくてもピンを立てられる方がべんりな気がします。(といってもlinkが定義されていないのはほんのわずかしかないですね)
ただ、linkがないものにピンを立てられるようにすると右側のピンを立てたもの一覧に表示するものがなくなっちゃうので困りそうですが….

setSiteinfoはよくない

おなじく8/24バージョンの追加機能のsetSiteinfoですが、これはよくないと思います。

まず現状では使い勝手がよくなかったです。
tumblr dashboard ではlinkが定義されていないのでsetSiteinfoで定義してピンが立てられるようにしたのですがsetSiteinfocommandの中でしか呼び出せないのでsiteinfoを変更するためだけにミニバッファからコマンドを呼び出す必要があって不便でした。ミニバッファにコマンドを追加するスクリプトがロードされたときに書き換えられるようにwindow.LDRize.setSiteinfoで呼び出せると使いやすくなると思います。

ですがsetSiteinfoはXPathをスクリプト本体に書くことになる点で好ましくないと思っています。
siteinfoのよいところは、スクリプトのコードかせページのスクレイピングのためのXPathが分離されたところだと思っています。ページのスクレイピングルールがコードから分離されて誰でも変更できるようになったことで

  • スクレイピングルールを他のスクリプトや異なる言語からでも利用できる
  • ページのデザインが変わってルールが壊れても、誰かが直してくれれば他のひとは壊れたことにも気づかないで使い続けられる
  • ページのデザインが変わるたびに新しいXPathを記述したスクリプトに入れ直す必要がない

という利点ができました。
setSiteinfoでコードの中にXPathを書いてしまうと、これらの利点が全部無くなってしまうのでsetSiteinfoでスクリプト固有のXPathを保持するよりも、それらもsiteinfoに混ぜてしまった方がよいと思います。siteinfoのサイズは大きくなりますが、あるスクリプトで使っているXPathを他のスクリプトでも参照したい、ということがあるでしょうし、それらがべつべつにメンテされるようになっちゃうのはもったいないと思います。

LDRizeでピンを立ててparagraphに対してコマンドを実行できるというのは、なんかFirefoxがOSでその上のシェルでコマンド実行しているみたいな感じがしました。この比喩で言ったらsiteinfoはディスクからビット列を読み出してきて、それを意味のあるかたちに組み立てるデバイスドライバみたいなものなのかなと思います。みんながそれぞればらばらにデバイスドライバを書くよりは、誰かが書いたのをみんなが使う方がハッピーだと思うのでsetSiteinfoではなくsiteinfoに項目を増やす方向で対応してほしいです。

そのほか細かいこと

siteinfoに項目を増やす、というのと同じはなしですが tumblr dashboard のようにlinkの中が画像だったりすることもあるのでURLを取り出すlinkと右側に表示するテキストをべつべつに記述できるようにしてもいいのではないでしょうか。デフォルトlinkのcontentTextで、ルールが記述されていればそれを使う、というような。

あとwindowのloadイベントではなく 実用 – ドキュメントのロード完了に合わせて関数を実行する に書かれているdocumentのDOMContentLoadedで実行されるほうが早く使えるようになるので便利だと思います。自分で書き換えてみたらちゃんと動かせなかったのでできるのかよくわかりませんけど….

感謝!

なんか文句ばかりになってしまいましたが、コマンドを好きに作って何でもできるというのは、ブラウザがOSに近くなったかんじでとても楽しいです。しかもそんなエッジなものにぴかぴかのみためがついていてほんとに感動しました。LDRize上でもっといろんなことができるようになるように、さらに自分もなにかつくっていこうとおもいます。