Category Archives: greasemonkey

greasemonkey

XMLがツリーで表示されない問題対策パッチ

Twitter / azu: AutoPagerizeが原因だったのか。長年悩まさ …経由MozillaZine.jp :: トピックを表示 – [解決済み]XMLのツリー表示についてでXMLツリーが表示されずにふつうのテキストファイルとして表示されてしまう問題の解決方法を知りました。

試してみたら確かにGreasemonkeyでスクリプトを実行しなければXMLツリーが表示されます。ページの内容がXMLのときにGreasemonkeyを実行したいということがないので、ページがXMLのときにはGreasemonkeyが実行されないように細工をするパッチを作りました。

--- greasemonkey.js.orig        2009-07-15 20:11:07.000000000 +0900
+++ greasemonkey.js     2009-07-15 20:17:13.000000000 +0900
@@ -109,6 +109,9 @@
     var href = new XPCNativeWrapper(unsafeLoc, "href").href;
     var scripts = this.initScripts(href);

+       if ( unsafeWin.document.contentType && !unsafeWin.document.contentType.match( /html/ ) )
+               return;
+
     if (scripts.length > 0) {
       this.injectScripts(scripts, href, unsafeWin, chromeWin);
     }

xml tree view

chrome extensionのつくりかた

追記 2010.6.4

この情報は古いので特集:先取り! Google Chrome Extensions|gihyo.jp … 技術評論社などをご参照ください。

しばらくチェックしてなかった間にchromiumでextensionが作れるようになっていました。

といっても、いまのところはchromiumのGreasemonkeyの仕様がちょっとだけ拡張されてextensionと呼ぶことにしたというだけで、自由度の点でFirefoxのものには遠く及びません。Greasemonkeyとの機能的な違いは3つくらい。

  • CSSを挿入させることができてstylishみたいに使える(未実装)
  • (明示的には書いてないけれど)自動的にアップデートできる(未実装)
  • ドキュメントが読み込まれた後だけでなく、ドキュメントが読み込まれる前に実行させることができる

作り方

作り方はChrome Extension HOWTO ‎(Chromium Developer Documentation)‎に書いてあります。使ったchromiumのバージョンはIndex of /buildbot/snapshots/chromium-rel-xp12074バージョン。

manifest.jsonを書く

はじめに適当なディレクトリを作って、その中にmanifest.jsonというextensionの名前やバージョンを定義するファイルを書きます。
Extensions
サンプルに従ってc:¥myextensionディレクトリを作ってmanifest.jsonには下の通りに書きました。

{
  "format_version": 1,
  "id": "0a65ddd0c050d1475a70c177a47909a9a47909a9",
  "version": "1.0",
  "name": "My First Extension",
  "description": "The first extension that I made."
}

extensionを実行するURLを正規表現もどきで書く所はGreasemonkeyのスクリプトと全く同じです。なんでこういう仕様なのかはThoughts on scaling – Chromium-dev | Google Groupsを読むとちょっと納得できます。おそらく今後どのイベントに反応してどうささせるかを柔軟に登録できるようになるのでしょう。

hello_world.htmlを書く

これべつにいらないんですけどextensionには(ちょっと前までのGreasemonkeyみたいに)jsファイルだけでなくhtmlファイルも入れることができます。

<h1>Hello, World!</h1>

動作確認

ここでまずextensionがちゃんと読み込まれるようになっているか確認するために、一度chromiumにロードさせてみます。
--enable-extensionsをつけて--load-extensionでextensionのディレクトリを指定して読み込ませます。

chrome.exe --enable-extensions --load-extension="c:\myextension"

うまく読み込めていればchrome-extension://0a65ddd0c050d1475a70c177a47909a9a47909a9/hello_word.htmlにアクセスするとこう表示されます。
Hello

スクリプトの記述

ちゃんとextensionが読み込まれているのを確認したら実際にページの内容を変更したりするためのコードを書いてみます。chromiumではこのスクリプトのことをcontent scriptと呼ぶそうです。

c:¥myextensionディレクトリに新しくfoo.jsを作って

document.images[0].src = "http://bit.ly/1293Af";
document.images[0].height = "auto";

ページが読み込まれたときにfoo.jsが実行されるようにmanifest.jsonに記述を追加します。

{
  "format_version": 1,
  "id": "0a65ddd0c050d1475a70c177a47909a9a47909a9",
  "version": "1.0",
  "name": "My First Extension",
  "description": "The first extension that I made.",
	 "content_scripts": [
    {
      "matches": ["http://www.google.co.jp/*"],
      "js": ["foo.js"]
    }
  ]
}

これでhttp://google.co.jp/を開くとロゴが置き換わる、はずなんですが、テストに使った2.0.171.0がはずれなのかなんなのか動きませんでした。
chrome-ui://extensions/で確認する限りではうまく読み込まれてるようなんですけど…

Chrome-Ui-Extensions-1

ドキュメントが読み込まれる前に実行させるには

manifest.json"run_at": "document-start"を追加することで、ドキュメントが読み込まれる前(おそらくheadが構築された後でbodyが構築される前)にスクリプトを実行させることができます。このタイミングでスクリプトを実行できると確かに便利なこともありますが、おそらくCSSを読み込むことを念頭に設定されたタイミングなのでしょう。

まとめ

いまのところchomiumのextensionはGreasemonkey(GM_xmlHttpRequestもまだドメインを超えられません)+Stylishの機能しかありません。

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を送ってください。

GearsMonkey on chrome/oAutoPagerizeで表示した写真を自動で全部ローカルに保存する

chromeにはもともとGears入ってるからGreasemonkey使えるようになったらGearsmonkey: Gears + Greasemonkeyでなんか遊べるかもー、と思って試行錯誤したらoAutoPagerizeLocalServerでロードした写真を全部ローカルに保存するっていうのができました。これはGearsのクロスドメイン制約の仕様上、実用性が低いんですけど、探せば何か便利な使い方ができるかもしれません。

oAutopagerize+LocalServer

もともとGearsはウェブアプリケーションでできることの幅を広げるためのものでしかないので、クロスドメイン制約が厳しくてあんまり自由にいろいろすることはできません。今回はページのコンテンツをキャッシュしてくれるLocalServerという機能を使って写真をローカルに保存するんですが、これがページと写真のURLのドメインが同じでないとキャッシュできない仕様になっています。ほとんどのサービスでは動的にページを生成するサーバと、静的に画像ファイルをサーブしてくれるサーバは別ドメインになっているので、たいていのサイトでは使えません(これまじめにオフラインで動くアプリ作ろうとした時にもけっこう不便だと思う)。

が、ストリートスナップ – Fashionsnap.comみたいにページと写真が同じドメインにあるやつだったらoAutoPagerizeLocalServerで写真をローカルに保存したりできます。

スクリプトのインストール

c:\scriptsoAutoPagerizefashionsnap.user.jsを入れます。
Scripts

Gearsの使用を許可する

oAutoPagerizefashionsnap.user.jsをインストールしてからストリートスナップ – Fashionsnap.comを開くと、一回目だけGearsを使うかどうかの確認が出るのでOKを押してGearsの使用を許可します。
Authme

ページをみていく

許可したらoAutoPagerizeでてきとうにページを見ていきます。
Fashionsnap

キャッシュを見に行く

てきとうにページを見たらキャッシュを見に行ってみましょう。キャッシュフォルダがプラットフォームによってまちまちみたいなんですがWindowsXPだとC:\Documents and Settings\ku\Local Settings\Application Data\Chromium\User Data\Default\Plugin Data\Google Gears\www.fashionsnap.com\http_80\streetsnap[4]#localserverにあります(kuのところはログインユーザ名)。

Cached

こんなかんじで読み込まれた写真がディスクに保存されます。Gearsのキャッシュなのでふつうのブラウザキャッシュと違ってGearsから明示的に消すまで消えません。

ドメインを越えてキャッシュできたら楽しいんですけどねー。

chromiumのgreasemonkey実装とスクリプトを再読み込みさせる方法

COLLECTION & COPY経由ChromiumのGreasemonkeyを軽く試した。でchromeにofficialなgreasemonkeyが搭載されたというのを知って、コードをチェック!

src/chrome/renderer/greasemonkey_slave.ccが実装。極めてシンプル。Firefox版と関数名が同じInjectScriptsになっていてうけました。

bool GreasemonkeySlave::InjectScripts(WebFrame* frame) {
  // TODO(aa): Check script patterns here

  for (std::vector<GreasemonkeyScript>::iterator script = scripts_.begin();
       script != scripts_.end(); ++script) {
    frame->ExecuteJavaScript(script->GetBody().as_string(),
                             script->GetURL().as_string());
  }

  return true;
}

src/chrome/renderer/render_view.cc

void RenderView::DidFinishDocumentLoadForFrame(WebView* webview,
                                               WebFrame* frame) {
  // Check whether we have new encoding name.
  UpdateEncoding(frame, webview->GetMainFrameEncodingName());

  // Inject any Greasemonkey scripts. Do not inject into chrome UI pages, but
  // do inject into any other document.
  if (greasemonkey_enabled_) {
    const GURL &gurl = frame->GetURL();
    if (gurl.SchemeIs("file") ||
        gurl.SchemeIs("http") ||
        gurl.SchemeIs("https")) {
      RenderThread::current()->greasemonkey_slave()->InjectScripts(frame);
    }
  }
}

で実行される。

これはWebKitのWebCoreを継承している

void WebFrameLoaderClient::dispatchDidFinishDocumentLoad()

の中からドキュメントがロードされたタイミング(HTMLがロードされて文字コードが再設定されるタイミング、ってかいてありました。DOM構築タイミングとの関係は調べてないです)だそうです。

ちなみにGreasemetalでは、WebKit内部の微妙なタイミングは外側からは取得する方法がないので500ms間隔でポーリングしてchromeの持っているタブの数をチェックして、新しく現れたタブにscriptを流し込む、という方法がとられています。

実装としてはGreaseKitGreasemetalと全く同じで単純にDOMが完成したタイミングでjavascriptのコードが実行されているだけでした。FirefoxのXPCNativeWrapperのような安全策を講じるのはJSエンジンに大きな細工が必要だし、パフォーマンスを落とすのでchromeでは今後も導入されない(つまり今後もGM_*系関数が使えるようになることはない)んじゃないかと思います。JSONPという裏技が編み出された今となっては昔に比べればクロスドメインXMLHttpRequestの必要性も減って、なくても困らなくなってるし(実際自分の使っているスクリプトで構造上GM_*が必須なものはないです)。

スクリプトの再読み込み

ついでにTwitter / Shogo Ohta: いちいち再起動するの面倒過ぎるのはなんとかならないのか調べてみたらRenderProcessHost::Init()というのが呼ばれた時にスクリプトを再読み込みしているので、一度同じレンダリングプロセス(chromiumのプロセスモデル参照。単純に言えば同じドメインのもの)に属するタブを全部閉じてからもう一度開くとスクリプトが再読み込みされるので、再起動しなくてもよくなります。
タブを閉じないでページの再読み込みをするとレンダリングプロセスが再利用されるけど、タブを閉じたらちゃんと消滅して再度開いた時にプロセスがまた生成されるのでスクリプトが再読み込みされて更新が反映されます。

一度タブを閉じてもう一度開いたら再読み込みされているのをGoogle Chrome にいよいよ GreaseMonkey がやってくる ::: creazy photographを参照してインストールしたchromium3804上で確認しました。

Twitter activity visualizer

Twitter activity visualizer is a Greasemonkey userscript namely visualizes users activity on twitter. You can install at Twitter activity visualizer – Userscripts.org.

Visualization results

Here are some average users visualization results.
This script counts number of tweets in each 10 minutes. Background color indicates when the tweet was posted. Blue denotes midnight, green denotes moring and yellow denotes evening.

average users

Average twitter users tweet only few times in each 10 minutes. We can find some pattern of twittering. They tend to Tweet at early morning and early evening(maybe they are tweeting when they come/leave office).

2

3

4

Not enough interesting visualizing average people.

the addicteds

Abnormal people are always entertaining us. Forget average people and lets find abnormal twitterer and see their activities.

5

this script reveals that the addicteds are living irregular and impulsive daily life. how useful.

1

See Also

FriendFeed activity visualizer (iGoogle gadget. introduction in Japanese)

微妙に新しくなったtumblrでReblogCommandを動くようにするためのパッチ

追記 2008.6.2

ReblogCommandの最新版はcodereposからインストールすることができます。

訂正 2008.4.13

reblogCommandのコード、間違ってたみたいです。修正してます。Tombloo0.1.6で動作確認済。

Tombloo 0.1.3、LDR + Tombloo – 実用でGreasemonkeyからTomblooの機能が利用できるようになっているので、それを使ってrelbogするようにするためのReblogCommandのパッチです。Tomblooの機能に依存しているのでTomblooがないと動かなくなります。

Tomblooにもパッチが必要です。Tomblooは extensions/tombloo/chrome/content/browser.xulgetReblogToken TUMBLR_URL を付け足すだけです。

--- browser.xul 2008-04-12 11:17:16.000000000 +0900
+++ browser.mine.xul    2008-04-12 16:24:51.000000000 +0900
@@ -40,7 +40,7 @@
                        GM_Tombloo.Tombloo.Service = update({}, env.Tombloo.Service,
                                'check share posters extracters'.split(' '));
                        GM_Tombloo.Tumblr = update({}, env.Tumblr,
-                               'post remove getInfo read reblog openTab getLoggedInUser'.split(' '));
+                               'post remove getInfo read reblog openTab getLoggedInUser getReblogToken TUMBLR_URL'.split(' '));
                        GM_Tombloo.FFFFOUND = update({}, env.FFFFOUND,
                                'post remove iLoveThis'.split(' '));
                        GM_Tombloo.Flickr = update({}, env.Flickr,

ReblogCommandのパッチは以下。パッチ済みのファイルをreblogcommand.user.jsに置いておきます。

あ、あと消すためのコマンドも入れました。でもバグっててピンをつけたのと違うやつが消えるっぽいので使わないでください!

pinned-or-current-link|deletePost

で消せます。

Tomblooのreblogメソッドで帰ってくるのがMochiKitのDeferredインスタンスなのですが、それをJsDeferredのDefferedListで扱う必要があるのでトリッキーなことをしています。こういうもともと繋がらないのをなんとかして繋げるの好きだなー。

--- reblogcommand.orig.js  2008-04-12 16:12:20.000000000 +0900
+++ reblogcommand.user.js  2008-04-13 11:15:38.000000000 +0900
@@ -86,26 +86,41 @@
 }
 
 function reblog(aURL){
-  var id  = getIDByPermalink(aURL);
-  var d;
-  with(D()){
-    d = Deferred();
-    if(!id) {
-      wait(0).next(function(){d.call()});
-      return d;
-    }
-  }
-  var url = getURLByID(id);
-  window.Minibuffer.status('ReblogCommand'+id, 'Reblog ...');
-  getSource(url).
-  next(function(res){
-    return postData(url, createPostData( parseParams( convertToHTMLDocument(res.responseText))));
-  }).
-  next(function(){ window.Minibuffer.status('ReblogCommand'+id, 'Reblog ... done.', 100); d.call()}).
-  error(function(){
-    if(confirm('reblog manually ? \n' + url)) reblogManually(aURL);
-    d.call();
+  return invokeTumblrMethod(
+    aURL, 'reblog', {
+      start:  'Reblog ...',
+      done:  'Reblog ... done.',
+      manual:  'reblog manually ? ',
+    }
+  );
+}
+
+function deletePost(aURL){
+console.log("deletePost", aURL);
+  return invokeTumblrMethod(
+    aURL, 'remove', {
+      start:  'Deleting ...',
+      done:  'Deleting ... done.',
+      manual:  'delete manually ? ',
+    }
+  );
+}
+
+function invokeTumblrMethod(aURL, methodName, msg) {
+console.log("invokeTumblrMethod", aURL);
+try {
+  window.Minibuffer.status('ReblogCommand'+aURL, msg.start);
+
+  var d = GM_Tombloo.Tumblr[methodName]( aURL ).addCallback ( function (res){
+    window.Minibuffer.status('ReblogCommand'+aURL, msg.done, 100);
+  }).addErrback( function(){
+    if(confirm( msg.manual + '\n' + url)) reblogManually(aURL);
+    d.callback();
   });
+console.log("invokeTumblrMethod", aURL, d);
+}catch(e) {
+  console.log(e);
+}
   return d;
 }
 
@@ -155,9 +171,8 @@
     window.Minibuffer.execute(target_cmd + ' | reblog -m' + clear_pin );
   }});
 
-window.Minibuffer.addCommand({
-  name: 'reblog',
-  command: function(stdin){
+
+var tumblrCommand = function(stdin, method, manualMethod){
     var args = this.args;
     var urls = [];
     if(!stdin.length){
@@ -175,23 +190,46 @@
     // reblog
     if(args.length = 1 && args[0] == '-m'){
       urls.forEach(function(aURL){
-        reblogManually(aURL);
+        manualMethod && manualMethod(aURL);
       });
     }else if(args.length){
       console.log('unknown args...');
     }else{
       urls = urls.filter(isTumblrUserURL);
       if(!urls.length) return stdin;
-      var lst = urls.map(reblog);
+      var lst = urls.map(method);
       if(lst.length > 1){
+        // JSDeferred/MochiKit Deferred compatibility hack.
+      var jsdl = {};
+      for ( var i = 0; i < lst.length; i++ ) {
+        var d = lst[i];
+        jsdl["jsd" + i] = d;
+        d.next = d.addCallback;
+        d.error = d.addErrback;
+      }
+
         with(D()){
-          parallel(lst).wait(2).
-          next(function(){window.Minibuffer.status('ReblogCommand','Everything is OK', 1000)});
+          var d = parallel(jsdl).wait(2).
+          next(function(){
+            window.Minibuffer.status('ReblogCommand','Everything is OK', 1000)
+        });
         }
       }
     }
     return stdin;
   }
+
+window.Minibuffer.addCommand({
+  name: 'reblog',
+  command: function (stdin) { return tumblrCommand.apply(this,[ stdin, reblog, reblogManually] ) }
 });
 
+window.Minibuffer.addCommand({
+  name: 'deletePost',
+  command: function (stdin) { return tumblrCommand.apply(this, [stdin, deletePost, null]) }
+});
+
+
 })()
+
+

SITEINFOのないページをAutoPagerizeするSITEINFO speculator for AutoPagerize version 0.0.2

Pagerization version 0.2.2 – ?D of Kに触発されてちょっと自分でも書いてみた。

AutoPagerizeよりもあとに実行されるようにインストールしておくと、AutoPagerizeでSITEINFOにマッチしなかったとき自動的にnextLinkpageElementを推測してAutoPagerizeを再実行します。

インストール

siteinfospeculator.user.js

つかいどころ

全部のページで使うとちょっと重たいので、Greasemonkeyの設定でincludeを*.tumblr.comで使ったりするといいとおもいます。tumblrのAutoPagerizeされてないテーマも、だいたい正しくAutoPagerizeできるかんじです。

フィードバック

SITEINFO speculatorでAutopagerizeが起動されたときは、右上の緑色がほんもののSITEINFOで起動されたときよりも暗くなります。マウスを持っていくと、自動生成されたSITEINFOの質がどうだったかのフィードバックを送るためのリンクが出てくるので、今後の開発の参考になるので、ひどいSITEINFOができてむかついたというときはmessyを、完璧で感動したというときはperfect, 一応動いてるけどみためがおかしくなるとかのときはokを押してもらえると助かります。

rating UI

SITEINFOのレイティングと一緒に、ページのURLと使っているブラウザのUserAgentと生成されたXPathが送信されます。送られたデータはRatingsからみることができます。もし間違っておくって消したい、というときにはここで削除してください。

アルゴリズム

nextLinkの検出は適当です。単純なOperaのFast Forwardアプローチで、”»”, “次”, “Older”, “older”, “Next”, “next”, “→”, “←”, “>”, “<”, “«”, “前”, “Prev”, “prev” をキーワードにして引っかかったテキストリンクだけを探しています。なのでnextLinkが画像だと機能しません。(version 0.0.2で画像もいちおう対応しました)

前にSITEINFOで調べたかんじでは、nextLinkは時系列の向きに関わらず右向きの矢印で示されることが多かったので、そういう並び方に変えたほうがよさそう。今はマッチしたキーワードのリンクを、テキストの長さに反比例するコスト関数で評価して、コストの小さいものをnextLinkにしてます。のページのほうが10 年の今日、Netscape Communications 社が mozilla.org を設立よりも優先されるように、という小細工。

pageElementXML::Diffのアルゴリズムをベースにしてます。Diffは正確に差分を出す必要があるけど、今回は差異があることだけわかればいいので、かなり簡略化して差異がある部分だけをみつけて、ツリーを下から上に差異を足しこんでいって、終わったところで、今度は上から差異の変化を見ていって差異のほとんどが含まれている要素を見つけてそれをpageElementにしています。0.5~1.0秒くらいブロックしちゃうかんじだけど、そこはyieldで書き直せば解決。

精度のほうはあんまり試してないけど、nextLinkが見つかったものに関しては5~6割くらい正しくpageElementを出せそう。

Pagerization version 0.2.2 – ?D of Kのリストを参考にいくつか試した結果は

http://1470.net/list/memo/recent
ok
http://2ch.ru/g/
nextLinkが見つけられない
http://www.adiumxtras.com/index.php?a=search&cat_id=3
ok
http://allabout.co.jp/relationship/secondmarriage/closeup/CU20071006A/index.htm
ok
http://www.altphotos.com/Gallery.aspx?browseby=toprated
ok(でも2ページしか出ない。AutoPagerizeのSITEINFOがあっても2ページしか出ないけど偶然?)
http://jp.techcrunch.com/
ok(アルゴリズム的には正しいけどレイアウトが乱れる)
http://mozillazine.jp/
ok
http://coderepos.org/share/changeset/
ok(ただしレイアウトが大きく異なっているChangeset 8044 – CodeRepos::Share – Tracで止まる)

かんそう

nextLinkを見つけるようなアルゴリズムがない。たくさんデータを集めて統計的に解決するしかなさそう。

Greasemonkeyが実行されてから実際にAutoPagerizeで次のページがロードされるまで時間があるので、Greasemonkeyでページの解析をしなくても、リモートのサーバにリクエストを送って差分を解析してもらうこともできる。これは基本的にブラウザで新しいページをロードするたびにリクエストが送られることになる。サーバ側はpathtraqと同じデータを得ることができる。だから今のpathtraqに欠けているインストールすることのインセンティブとして、nextLinkpageElementを返してくれるAPIとかどうかなーと思ったり。pageElementの検出はWebページの本文抽出にも似ているところがあって、ますますpathtraqと相性いいなと感じました。

検出精度を高めていくには自動でテストをまわしてカバレッジを出してくれる何かがないとつらいです。perlのWWW::Mechanize::AutoPagerHTML::TreeBuilder::XPathでSITEINFOをパースしてみたらXPathの評価で例外が出まくってだめでした。PHPのXMLまわりはクソでwell-formedにするのも一苦労なので、ruby, pythonをまともに使えない自分には手詰まり。

PrivilegedMonkeyをFirefox3で動くようにした

Greasemonkeyに拡張機能でないとできないことをする関数を追加するPrivilegedMonkeyをFirefox3対応にしました。バージョンあげたりしない気もするけどちゃんとFirefox3のsecure update対応にした。

なんかさいきんVous y êtes !*のフィードのファイルがダウンロードできないけどページにリストされてるやつはダウンロードできるという状態で困ってたので。こんなかんじで使いますよ。save.localにはダウンロードしたいURLを文字列かDOMのattributeで渡します。

Greasemonkeyに拡張機能でないとできないことをする関数を追加するPrivilegedMonkeyのページからダウンロードできます。

作る側の立場としてはセキュアアップデートを提供しないとだめだと思いますが、使う側としては当分の間はめんどくさそうなのでFirefox3で動かない拡張機能を動くようにするでバージョンとセキュアアップデートのチェックを外して使ってます。

The art of AutoPagerize nextLink XPath writing

SITEINFOのnextLinkケーススタディ – 0x集積蔵を読んで自分が書いたのを思い出したら、これを公開したつもりで公開してませんでした…

os0xさんの書かれた記事のほうが網羅的なので、あちらを読んでもらえばよいですが、兄弟要素から取得するケースnumberaの中の数字を利用することができるときもある、というのだけいいたいので公開しておきます。

AutoPagerizeのSITEINFOを書いていると、ナビゲージョン部分のHTMLがださくてnextLinkをXPathで表現できなさそうなことがよくあります。

が、ふたつほどコツを見つけたのでご紹介。

following-sibling, descendant-or-selfを使う

ありがちなのがナビゲーションに次のページへのリンクがなくて、数字だけが並んでいて今いるページだけハイライトされてるタイプのやつです。
Behance :: Make Ideas Happenのナビゲーションがそれでした。みたいめがこんなやつ。

Behance navigation

Behanceの場合HTMLはきれいでこんなふうになっています。

behance navigation HTML

この場合は楽勝。spanの次にあるaを表すXPathを作ればokです。

//span[@class="pages"]/following-sibling::a[1]

HTMLによってはspanaが同じ親を持っていなくてaがさらにfont(!)なんかで囲まれていることもありますが、そのときはdescendant-or-selfを組み合わせて使えば大丈夫。

numberを使う

nextLink固有の特徴なので他のときには使えませんが、ページのナビゲーションなのでたいていは末端の要素に数字が入っています。それを利用します。さっきのBehanceを例に。今見ているページの数字に1足したものを取り出すというアプローチができます。

//a[text() = number( //span[@class='pages']/text() ) + 1 ]

Userscripts.orgの検索結果ページはナビゲーション部分のHTMLが

みたいに、ひどいことになっていて、現在のページを表す要素を取り出すことが困難です。でもnumberを使うとわりとかんたん。

id("content")/p[last()]/node()[not(self::a) and ( number(self::text()) > 0  ) ]/following-sibling::a[1]

pの子要素でaじゃないやつで数値として評価したときに0より大きくなるやつの次にあるaというふうに表現できます。かんたん。ただ1ページ目のときはPages: とページ番号のところがくっついてひとつのテキストノードになって

number("Pages: 1")が0を返すので、Pages:の部分はあらかじめ取り除いておく必要があります。それを加えて

id("content")/p[last()]/node()[not(self::a) and ( number(self::text()) > 0 or number( substring-after(self::text(), "Pages:") ) > 0 ) ]/following-sibling::a[1]

にすればかんぺき。

ちなみにnumberの仕様は、文字列の先頭にある空白文字は全部無視、数値列のあとにある空白も全部無視するという仕様。atoiは数値列のあとに数値じゃない非空白文字があっても、それ以前の部分を値として評価してくれますがXPathのnumberはそういうときはNaNになるという仕様で、Firefoxでの実装もそうなっています。

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.

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( ...... );
    }
};

key code love FLASH KEY and Full Key Codes

実用 – 入力したキーを表示するGreasemonkeyスクリプト

前にも Full Key Codes なんていうのを書いてるけど、このてのやつがなぜか好きみたいで、キーを表示している部分のコードを外側から呼べないかと思ってインストールしたのだけど、みためのかわいさにやられてそのままにしてる。検索したりするときに文字を入れると、そのたびに表示されるものが健気にカチカチかわるところがかわいい。

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のミニバッファにreblogコマンドを追加するスクリプト LDRize_tumbler_dashboard_reblog.user.js

このページのスクリプトは最新のLDRizeでは動かなくなっているので LDRizeのminibuffer用reblogコマンド LDRize_tumblr_reblog.user.js 8.28版対応バージョン にあるものをご利用ください。

LDRizeというGreasemonkeyのスクリプトがあります。AutoPagerize – Userscripts.orgと同様にページごとにXPathでルールを記述することで任意のサイトを Livedoor Reader と同じようにjで次、pでピンをつけてoで全部開けるというUIを実現するものです。

自分は Livedoor Reader を使っていないので、ヘーくらいにしか思ってなかったんですが昨日試してみて感動。jkpoだいぶんいいかんじになってきたところですが、それ以前にいちいちみためがかっこよくて、さらにピンを立てたものに対して任意のコマンドを実行できるというところにまだ見ぬ何かが見えてくるはずだというわけで、試しにピンを立てたものをreblogする長い名前のGreasemonkeyスクリプトをつくりました。

つかいかた

まず下のスクリプトをインストールします。

LDRizeよりも後に実行されるように設定してください。(後からインストールしたスクリプトが後に実行されるので、もうLDRizeが入っていればインストールするだけで、順番は気にしなくてokです)

そうするとダッシュボードのLDRizeのminibufferに tumblr_dashboard_reblogreblog というコマンドが追加されます。

まずページのロードが完了したら tumblr_dashboard_reblog を実行します。いまのLDRizeのsiteinfoではdashboardでピンを立てることができないので、そのsiteinfoを変更してピンを立てられるようにするために実行します。

あとはふつうにjで進めてreblogしたいとおもったものをpでピンを立てていきます。ふつうは右にピンを立てたページの名前が表示されていくのですがLDRizeの仕様上reblogのアイコンが並んでいきます。

最後にminibufferを開いてreblogコマンドを実行すると、実際にreblogが行われます。

tumblrにやさしく2秒おきにreblogするので時間がかかります。
Firebugが入っていればFirebugのコンソールにreblogが完了したときにURLが表示されます。

なのですが、なんかLDRizeが悪いのか自分のスクリプトが悪いのかfirefoxのXPath実装に問題があるのか、たくさんreblogしようとするとダッシュボードの一番上にあるものがピンを立てた個数分reblogされるという問題が出たりするのでおきをつけください。paragraphをコンテキストにしてlinkが評価されるはずなのがなぜかdocumentをコンテキストにして評価されてしまうのが原因なところまでわかりましたが、なぜそうなるときがあるのかはわかっていません。

でもLDRizeでなにができるか、ちょっと体験してみるにはよいと思います。

おわび

reblogする部分のコードはライセンスについて特に書かれていなかったけど、みんな ShareOnTumblr.js からreblogみたいなもんだろくらいでちぎって持ってきています。すいません。

Greasemonkey extension のコードから実行時のスコープを調べる

Greasemonkey script execution environment – Chagama Lab でGreasemonkeyのスクリプト実行時のスコープがどうなっているかの話が書かれていました。自分もAutoPagerizeのコードを読んだときに、コードがfunctionで囲まれているのを見てブックマークレット同様そう書かないとだめなものなんだなー(ほかのスクリプトand/orウインドウとスコープが共有されている)、と思ってたので、そんなことないよ、と書かれているのは新鮮でした。

いい機会なのでどうなっているのかはっきりさせておくべくGreasemonekyのextensionのコードをのぞいてみたらシンプルなつくりになっていてすぐ分かりました。

Greasemonkeyのバージョンは0.7.20070607です。
はじめに chrome/chromeFiles/content/browser.jsGM_BrowserUI.chromeLoad でGreasemonkeyの実行時に必要なイベントをフックしています。

  this.appContent = document.getElementById("appcontent");
  .... snip ....
  // hook various events
  GM_listen(this.appContent, "DOMContentLoaded", GM_hitch(this, "contentLoad"));
  GM_listen(this.contextMenu, "popupshowing", GM_hitch(this, "contextMenuShowing"));
  GM_listen(this.toolsMenu, "popupshowing", GM_hitch(this, "toolsMenuShowing"));

AutoPagerizeは、ベージのロードが終わったあとで有効になるんだなー、と思ってたのですが Greasemonkeyが実行されるタイミングが DOMContentLoaded だからと分かったのは思わぬ収穫。

このイベントから2段階くらい関数が呼ばれて、最終的にuser.jsを呼び出しているのは components/greasemonkey.jsinjectScripts でした。

      sandbox = new Components.utils.Sandbox(safeWin);

      logger = new GM_ScriptLogger(script);

      console = firebugConsole ? firebugConsole : new GM_console(script);

      storage = new GM_ScriptStorage(script);
      xmlhttpRequester = new GM_xmlhttpRequester(unsafeContentWin,
                                                 appSvc.hiddenDOMWindow);

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

      // hack XPathResult since that is so commonly used
      sandbox.XPathResult = Ci.nsIDOMXPathResult;

      // add our own APIs
      sandbox.GM_addStyle = function(css) { GM_addStyle(safeDoc, css) };
      sandbox.GM_log = GM_hitch(logger, "log");
      sandbox.console = console;
      sandbox.GM_setValue = GM_hitch(storage, "setValue");
      sandbox.GM_getValue = GM_hitch(storage, "getValue");
      sandbox.GM_openInTab = GM_hitch(this, "openInTab", unsafeContentWin);
      sandbox.GM_xmlhttpRequest = GM_hitch(xmlhttpRequester,
                                           "contentStartRequest");
      sandbox.GM_registerMenuCommand = GM_hitch(this,
                                                "registerMenuCommand",
                                                unsafeContentWin);

      sandbox.__proto__ = safeWin;

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

最後のevalInSandboxでコードをfunctionで囲んでくれているので確かにuser./js側でfunctionで書くのは意味がないと言えます。このComponents.utils.SandboxComponents.utils.evalInSandbox – MDCによるとFirefox1.5から導入されたそうで
その名の通りjsの実行時にsandboxを作って実行してくれるもの。コードを見るとsandbox.windowにブラウザのウインドウのwindowを代入しているけれど Greasemonkey script execution environment 2 – Chagama Labに書かれている

Global オブジェクト != window オブジェクトであることも分かる

という結果になっているということは、sandboxが勝手にwindowというプロパティをグローバルスコープにしたりはしないということみたいですね。

Greasemonkeyのコードはextensionシロウトでも読めるようなつくりだったので疑問があったらのぞいてみればわかりそうです。