素人がプログラミングを勉強していたブログ

プログラミング、セキュリティ、英語、Webなどのブログ since 2008

連絡先: すかいぷ:javascripter_  か javascripter あっと tsukkun.net skypeのほうがいいです

AutoPageLoaderというGreasemonkeyスクリプトをつくりました。

さらにさらにさらに追記:最新版は
/lang/javascript/userscripts/autopageloader.user.js –
CodeRepos::Share – Trac
です

さらにさらに追記:addDocumentFilter実装しました。
AutoPagerizeのとは少し違って、contentDocumentを渡しているだけなのでgetElement系が使えたりしていい感じです。

さらに追記:DOMContentLoadedにすると、他のGreasemonkeyスクリプトがiframe内に読み込まれる前に実行されてしまう模様。addDocumentFilterの実装を検討してみます。

追記:id:os0xさんのコメントを見て、実行タイミングをloadからDOMContentLoadedにしてみました。
GoogleImagesではきちんと動いていると思います。
ちなみに、DOMFrameContentLoadedを使うと必要のないイベントがキャッチされてしまう可能性があるのでDOMContentLoadedにしました。


いわゆるAutoPagerizeクローン。似たようなものは結構たくさんあるけど、自分のも作っておきたい。

このスクリプトは、AutoPagerizeと比較して、下記の点がすぐれてます。

軽い

余計な機能がついていないので軽いです。

microformats対応

デフォルトでAutoPagerize独自のmicroformats、hAtom、xFolkに対応しています。また、自由にmicroformatsを追加することもできます。
XPathはjAutoPagerizeを参考にしましたが、多少変更しています。
SITEINFOの読み込みは、ローカルのSITEINFO、microformats、wedataの順です。

GoogleImagesやYahoo!にも対応

AutoPagerizeと決定的に違うのがこの部分です。
JavaScriptによって生成される要素も検索対象となるので、GoogleImagesでも動作します。

また、Yahoo!のように、次ページのURLのサブドメインが違っていても動きます。

ほかのGreasemonkeyスクリプトとバッティングしない

iframe内でGreasemonkeyスクリプトを実行してから次ページの要素を取得するので、基本的にはAutoPagerizeのようにフィルターAPIに登録する必要がありません。(Greasemonkeyスクリプトの実行タイミングはAutoPagerizeのaddDocumentFilter相当)
AutoPageLoaderのiframeでGreasemonkeyスクリプトを実行したくない場合、そのスクリプトの先頭に

if(window.name=='autopagerize_iframe')return;

してください。

AutoPagerizeと互換性のあるAPI

AutoPagerize.addFilterを搭載しています。ですが、addDocumentFilter相当のものがデフォルトで動作するので必要になる場合はあまりありません。addDocumentFilterもあります。

参考にしたもの

  1. id:swdyhさん作成のAutoPagerize for Greasemonkey
  2. id:cho45さん作成のjAutoPagerize for Greasemonkey

**困っていること

LDRizeと併用することはできるが、ストライプがうまくいかない。

LDRizeはけっこう巨大なので、何が原因なのか調べ切れませんでした。
きちんと動きます。

スクリプト本体

// ==UserScript==
// @name           AutoPageLoader
// @namespace      http://d.hatena.ne.jp/javascripter/
// @include        http*
// ==/UserScript==

(function(){
if(window.name=='autopagerize_iframe')return;

var Class = function(){return function(){this.initialize.apply(this,arguments)}}

var AutoPageLoader=new Class();
AutoPageLoader.prototype={
  Settings:{
    siteinfo_urls:[
      'http://wedata.net/databases/AutoPagerize/items.json'
      ],
    AUTO_START:true,
    REMAIN_HEIGHT:300,
    LOCAL_SITEINFO:[
      {
        name: 'GoogleImages',
        data: {
          pageElement: 'id("ImgContent")',
          url: '^http:\/\/images.google.co.jp\/images\?',
          nextLink: '//td[@class="b"]/a',
          exampleUrl: 'http:\/\/images.google.co.jp\/images?q=hatena'
        }
      }],
    MICROFORMATS:[
      {
        name:'autopagerize',
        data:{
          url:'^https?://.*',
          nextLink:'(//a | //link)[@rel="next"]',
          insertBefore:'//*[contains(concat(" ",@class," "), " autopagerize_insert_before ")]',
          pageElement:'//*[contains(@class, "autopagerize_page_element")]'
          }
      },
      {
        name:'hAtom',
        data:{
          url:'^https?://.*',
          nextLink:'(//a|//link)[contains(concat(" ",normalize-space(translate(@rel, "NEXT", "next"))," "), " next ")]',
          insertBefore:'//*[contains(concat(" ",@class," "), " hfeed ")]/following-sibling::node()',
          pageElement:'//*[contains(concat(" ",@class," "), " hfeed ")]'
        }
      },
      {
         name:'xFolk',
         data:{
           url:'^https?://.*',
           pageElement:'//*[contains(concat(" ",@class," "), " xfolkentry ")]',
           insertBefore:'//*[contains(concat" ",@class," "), " xfolkentry ")]/following-sibling::node()',
           nextLink:'(//a|//link)[contains(concat(" ",normalize-space(translate(@rel, "NEXT", "next"))," "), " next ")]',
         }
      }
    ],
    color:{
      on:'#00cc00',
      off:'#cccccc',
      loading:'#0000cc',
      finish:'#cccc00',
      error:'#cc0000'
    }
  },
  addFilter:function(callback){
    this.filters.push(callback);
  },
  addDocumentFilter:function(callback){
    this.documentFilters.push(callback);
  },
  getCache:function(callback){
    var cache=GM_getValue('cache');
    cache?
      callback(eval(cache)):
      this.setCache(callback);
  },
  setCache:function(callback){
    var siteinfo=[];
    this.Settings.siteinfo_urls.forEach(
      function(url,i,siteinfo_urls){
        GM_xmlhttpRequest({
          method:'GET',
          url:url,
          onload:function(res){
            siteinfo.push(eval(res.responseText));
            if(siteinfo.length==siteinfo_urls.length){
              GM_setValue('cache',uneval(siteinfo));
              callback(siteinfo);
            }
          }
        });
      });
  },
  clearCache:function(){
    GM_setValue('cache','');
    this.getSiteinfo(function(data){this.siteinfo=data});
  },
  getSiteinfo:function(callback){
    var self=this;
    this.getCache(function(siteinfo){
      siteinfo.unshift(self.Settings.LOCAL_SITEINFO,self.Settings.MICROFORMATS);
      var data=null;
      siteinfo.some(
        function(siteinfo){
          for each(var {data:i} in siteinfo){
            if(
              RegExp(i.url).test(location.href)&&
              document.evaluate(i.nextLink,document,null,XPathResult.BOOLEAN_TYPE,null).booleanValue&&
              document.evaluate(i.pageElement,document,null,XPathResult.BOOLEAN_TYPE,null).booleanValue){
              data=i;
              return true;
            }
          }
          return false;
        });
    callback(data);
    });
  },
  bindScroll:function(){
    var self=this;
    return function(){
      if(!self.enabled||self.loading)return;
      if(self.appendPoint.getBoundingClientRect().bottom-window.innerHeight<self.Settings.REMAIN_HEIGHT)
        self.setNextPage();
    }
  },
  bindDblclick:function(){
    var self=this;
    return function(){
      self.enabled=!self.enabled;
      self.changeState(self.enabled?'on':'off');
    }
  },
  setNextPage:function(){
    var self=this;
    self.loading=true;
    var nextLink=this.lastDocument.evaluate(this.siteinfo.nextLink,this.lastDocument,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue;

    if(!nextLink){
      self.changeState('finish');
      return;
    }
    var nextUrl=nextLink.href;
    self.changeState('loading');
    var iframe=document.body.appendChild(document.createElement('iframe'));
    iframe.contentWindow.name='autopagerize_iframe';
    iframe.src=nextUrl;
    iframe.style.display='none';
    iframe.contentWindow.addEventListener('DOMContentLoaded',
      function(){
      self.loading=false;
      self.changeState('on');
      self.lastDocument=iframe.contentDocument;
      self.documentFilters.forEach(function(callback){
        callback(self.lastDocument,nextUrl,self.siteinfo);
      });
      var pageElement=self.lastDocument.evaluate(self.siteinfo.pageElement,self.lastDocument,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
      if(pageElement.snapshotLength==0)
        self.changeState('error');

      var pageNavi=document.createElement('div');
      pageNavi.innerHTML='page: <a class="autopagerize_link" href="'+nextUrl+'">'+(++self.pages);
      var insertPoint=null;
      if(self.siteinfo.insertBefore)
        insertPoint=document.evaluate(self.siteinfo.insertBefore,document,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue;
      var addElement=insertPoint?
        function(element){
          return insertPoint.parentNode.insertBefore(element,insertPoint);
        }:
        function(element){
          return self.appendPoint.parentNode.appendChild(element);
        };

      addElement(pageNavi);
      var pages=[];
      for(var i=0;i<pageElement.snapshotLength;i++)
        pages.push(addElement(document.importNode(pageElement.snapshotItem(i),true)));

      self.filters.forEach(function(filter){filter(pages)});
      self.appendPoint=pages[pages.length-1];
      iframe.contentWindow.removeEventListener('load',arguments.callee,false);
      setTimeout(function(){iframe.parentNode.removeChild(iframe)},0);
      },false);
  },
  changeState:function(state){
    var self=this;
    this.icon.style.backgroundColor=this.Settings.color[state];
    switch(state){
      case 'finish': case 'error':{
        window.removeEventListener('scroll',this.onscroll,false);
        window.removeEventListener('scroll',this.ondblclick,false);
        setTimeout(function(){self.icon.parentNode.removeChild(self.icon)},1000);
        break;
      }
    }
  },
  addIcon:function(){
    this.icon=document.body.appendChild(document.createElement('div'));
    this.icon.id='autopagerize_icon';
    with(this.icon.style){
      top='5px';
      right='5px';
      width='10px';
      height='10px';
      position='fixed';
      zIndex='9999';
    }
    this.changeState(this.Settings.AUTO_START?'on':'off');
  },
  setMenu:function(){
    var self=this;
    GM_registerMenuCommand('AutoPageLoader - clear cache', function(){self.clearCache.call(self)});
  },
  initialize:function(){
    var self=this;
    this.getSiteinfo(
      function(data){
        if(!data)return;
        self.siteinfo=data;
        self.enabled=self.Settings.AUTO_START;
        self.filters=[];
        self.documentFilters=[];
        self.pages=1;
        self.loading=false;
        self.lastDocument=document;

        var elems=document.evaluate(data.pageElement,document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
        self.appendPoint=elems.snapshotItem(elems.snapshotLength-1);

        self.onscroll=self.bindScroll();
        self.ondblclick=self.bindDblclick();
        window.addEventListener('scroll',self.onscroll,false);
        window.addEventListener('dblclick',self.ondblclick,false);

        self.addIcon();
        window.AutoPagerize={
          addFilter:function(callback){self.addFilter.call(self,callback)},
          addDocumentFilter:function(callback){self.addDocumentFilter.call(self,callback)}
        };
        self.setMenu();
      });
  }
}
new AutoPageLoader();
})();
//.user.js