Yet Another Hatena Referer Viewer

1/23の日記で公開したHatena Referer Viewerをさらにバージョンアップした。変更点は以下の通り:

  • JAN/EANページに対応した
  • getNodesFromXPath()と$X()がapplication/xhtml+xmlなページで動かないというバグ(このスクリプトでは元々実害なし)があったので修正した
  • 全体的にプログラムの構造をリファクタリングした
  • 元のスクリプトと名前が同じだと紛らわしいので変更した(頭にYet Anotherを付けただけの安直なネーミングだけど)

YetAnotherHatenaRefererViewer.user.js

// ==UserScript==
// @name           Yet Another Hatena Referer Viewer
// @namespace      http://d.hatena.ne.jp/nozom/
// @author         nozom <nozom.kaneko@gmail.com>
// @description    Rearranges Hatena Diary's referer view.
// @include        http://d.hatena.ne.jp/*
// @include        http://*.g.hatena.ne.jp/*
// ==/UserScript==

// This script was originally published by motormean <motemen@gmail.com> at the following pages:
//   - http://tokyoenvious.xrea.jp/b/software/firefox/hatena_ref_view.html
//   - http://tokyoenvious.xrea.jp/b/software/firefox/hatena_ref_view_2.html

// ver 2007-01-29

(function() {
    /*===== 設定 =====*/

    // はてなキーワード //
    var patKeywords = {
        '^http://(d2?|diary).hatena.ne.jp/keyword(mobile|diary|stats)?/([^\\?]+).*': '$3',
        '^http://(\\w+).g.hatena.ne.jp/keyword(mobile|diary)?/([^\\?]+).*': '[group:$1] $3',
        '^http://b.hatena.ne.jp/keyword/(.+)': '?B $1',
        '^http://r.hatena.ne.jp/keyword/([^/]+)$': '?R $1',
    };

    // はてなダイアリー・グループ //
    var patDiaries  = {
        '^http://(d|diary).hatena.ne.jp/([\\w-]+)/.*': 'id:$2',
        '^http://(\\w+).g.hatena.ne.jp/([\\w-]+)/.*': 'g:$1:id:$2',
        '^http://(\\w+).g.hatena.ne.jp/.*': 'g:$1',
        '^http://(d|diary).hatena.ne.jp/http\\?(.*)': '?D url(http:$2)',
    };

    // アンテナ //
    var patAntennae = {
        '^http://(a|antenna).hatena.ne.jp/([\\w-]+)/.*': '?A(id:$2)',
        '^http://(a|antenna).hatena.ne.jp/([^/]*)': '?A $2',
        '^http://i-know.jp/([\\w-]+).*': '?i-know($2)',
    };

    // ブックマーク //
    var patBookmarks = {
        '^http://b.hatena.ne.jp/t/([\\w-]+).*': '?B tag:$1',
        '^http://b.hatena.ne.jp/t\\?.*tag=([^&]*).*': '?B tag:$1',
        '^http://b.hatena.ne.jp/entry/(.+)': '?B ($1)',
        '^http://b.hatena.ne.jp/([\\w-]+)/$': '?B(id:$1)',
        '^http://b.hatena.ne.jp/([\\w-]+)(/.+)': '?B(id:$1) $2',
        '^http://b.hatena.ne.jp/(hotentry|entrylist.*)': '?B $1',

        '^http://clip.livedoor.com/clips/([^/]*)(.*)': 'livedoor clip($1) $2',
        '^http://del.icio.us/([^/?]*)(/.+)': 'del.icio.us($1) $2',
        '^http://del.icio.us/([^/?]*)?.*': 'del.icio.us($1)',
    };

    // RSSリーダー //
    var patRSS = {
        '^http://r.hatena.ne.jp/([\\w-]+)/$': '?R(id:$1)',
        '^http://r.hatena.ne.jp/([\\w-]+)/(.+?)/': '?R(id:$1) /$2/',
        '^http://r.hatena.ne.jp/([\\w-]+)/(.+?)/(http.+)': '?R(id:$1) /$2/ ($3)',
        '^http://r.hatena.ne.jp/([\\w-]+)/(http.+)': '?R(id:$1) ($3)',
        '^http://keyword.livedoor.com/w/(.*)': 'LDR(keyword:$1)',
    };

    // はてなアイデア //
    var patIdeas = {
        '^http://i.hatena.ne.jp/idea/(\\d+)': '?I:$1',
    };

    // その他はてな内 //
    var patHatena = {
        '^http://(d|diary)?.hatena.ne.jp/?([\\w-]*)': '?D $2',
        '^http://(\\w+).g.hatena.ne.jp/?([\\w-]*)': '?G($1) $2',
        '^http://g.hatena.ne.jp/?([\\w-]*)': '?G $1',
        '^http://f.hatena.ne.jp/?(\\w*)': '?F $1',
        '^http://www.hatena.ne.jp/(\\d+)/': 'question:$1',
    };

    // 検索エンジン //
    /* tDiary-users Project Wiki内
     * http://tdiary-users.sourceforge.jp/cgi-bin/wiki.cgi?referer_table
     * から一部コピーさせてもらいました。
     * ライセンス: Creative Commons License
     * http://creativecommons.org/licenses/by-nc/1.0/
     */
    var patSearches = {
        '^http://search.hatena.ne.jp/(web)?search\\?word=([^&]*)(&.*)?': '?Search($2)',

        '^http://www.google.([^/]+)/.*?(\\Wq|as_q)=([^&]*).*': 'Google($3)',
        '^http://www.google.([^/]+)/search.*?(\\Wq|as_q)=cache:[^:]*:([^\\s\\+]+)(\\s|\\+)([^&]*).*': 'Google($5) [Cache:$3]',
        '^http://blogsearch.google.com/blogsearch.*?\\Wq=([^&]*).*': 'Google($1)',
        // '^http://images.google.([^/]+)/.*?imgurl=([^&]*).*': 'Google image($2)',
        // '^http://\\w+.google.([^/]+)/search.*?(\\Wq|as_q)=cache:[^:]*:([^\\s\\+]+)(\\s|\\+)([^&]*).*': 'Google($5) [Cache:$3]',
        // '^http://216.239.\\d+.\\d+/.*?(\\Wq|as_q)=cache:[^:]*:([^\\s\\+]+)(\\s|\\+)([^&]*).*': 'Google($4) [Cache:$2]',
        // '^http://\\w+.google.([^/]+)/.*?(\\Wq|as_q)=([^&]*).*': 'Google($3)',
        // '^http://216.239.\\d+.\\d+/.*?(\\Wq|as_q)=([^&]*).*': 'Google($2)',
        // '^http://google/search.*?\\Wq=([^&]*).*': 'Google($1)',

        '^http://([^\.]*\.)?search.yahoo.(com|co\\.jp)/.*?p=([^&]*).*': 'Yahoo($3)',
        '^http://new.search.yahoo.([^/]+)/.*\\Wp=([^&]*).*': 'Yahoo($2)',
        '^http://websearch.yahoo.(com|co\\.jp)/.*?p=([^&]*).*': 'Yahoo($2)',

        '^http://(www|search|ocn(search)?|blog|mixi.search).goo.ne.jp/.*?MT=([^&]*).*': 'goo($3)',

        '^http://.*?livedoor.com/.*?search\\?q=([^&]*).*': 'livedoor($1)',

        '^http://.*search.msn(\\.[^/]+)/.*?[\\?&]x(q|MT)=([^&]*).*': 'msn($3)',
        '^http://.*search.msn.*?[\\?&](q|MT)=([^&]*).*': 'msn($2)',

        '^http://(www|ocn|apple|so-net|dion|odn|hi-ho|sleipnir).excite.co.jp/.*?(search|s)=([^&]*).*': 'excite($3)',

        '^http://.*?matome.jp/(tag|keyword)/(.*?)(.html)?$': 'matome.jp($2)',

        '^http://(search.)?(www.)?infoseek.co.jp/.*?qt=([^&]*).*': 'Infoseek($3)',
        '^http://search.odn.ne.jp/.*?(QueryString|key)=([^&]*).*': 'ODN($2)',
        '^http://.*?lycos.(co\\.jp|com)/?.*(query|qt?)=([^&]*).*': 'Lycos($3)',
        '^http://search.fresheye.com/.*?kw=([^&]*).*': 'FreshEye($1)',
        '^http://(a?search|www).nifty.com/.*?(q|Text)=([^&]*).*': '@nifty($3)',
        '^http://odin.ingrid.org/.*?key=([^&]*).*': 'ODiN($1)',
        '^http://search.naver.co.jp/search.naver.*?query=([^&]*).*': 'NAVER($1)',
        '^http://cgi.search.biglobe.ne.jp/cgi-bin/.*?q=([^&]*).*': 'BIGLOBE($1)',
        '^http://www.ceek.jp/.*q=([^&]*).*': 'ceek.jp($1)',
        '^http://.*?.aol.com/.*query=([^&]*).*': 'AOL Search($1)',
        '^http://www.euroseek.com/.*string=([^&]*).*': 'euroseek.com($1)',

        // '^http://(www.)?technorati.jp/search/search.html.*\\?.*query=([^&]*).*': 'technorati.jp($2)',
        '^http://(www.)?technorati.jp/search/([^\?&]*).*': 'technorati.jp($2)',
        '^http://(www.)?technorati.com/search/([^\\?&]*).*': 'technorati.com($2)',
        '^http://(www.)?technorati.com/tags?/([^\\?&]*).*': 'technorati.com(Tags:$2)',

        '^http://www.yahoogle.jp/(.*).html': 'yahoogle($1)',

        '^http://ask.jp/blog.asp\\?.*?q=([^&]*).*': 'ask.jp($1)',
        '^http://(cybozu|duogate).excite.co.jp/search.gw\\?.*search=([^&]*).*': 'excite($2)',
        '^http://search.blogger.com/.*\\?.*?q=([^&]*).*': 'blogger($1)',
        '^http://bulkfeeds.net/.*?q=([^&]*).*': 'bulkfeeds($1)',

        // '^http://crooz.jp/ex/([^/]+)/k/proxy_i.jsp\\?(.*)query=([^&]*).*': '$1($3)',
        // '^http://crooz.jp/k/search.jsp\\?.*query=([^&]*).*': 'crooz($1)',

        '^http://bst.blogpeople.net/search_result.jsp?.*keyword=([^&]*)': 'blogpeople($1)',
        '^(http://trendlink.mirailab.com/.*)': 'trendlink($1)',
        '^(http://tl.milabo.net/.*)': 'trendlink($1)',
        // '^(http://animemo.seesaa.net/article/.*)': 'animemo($1)',
        '^(http://www.blognavi.com/entry/.*)': 'blognavi($1)',
        '^(http://(www.)?1470.net/bm/urlinfo/.*)': 'blogmap($1)',
        // '^(http://clipsubject.com/ac/.*)': 'clipsubject($1)',

        '^http://ezsch.ezweb.ne.jp/search/.*query=([^&]*).*': 'ezsch($1)',

        '^http://www.namaan.net/result?.*query=([^&]*).*': 'NAMAAN($1)',

        // 取りこぼし
        // '^http://([^\\?/]+).*\\?.*&?(q|p|search|key)=([^&]*).*': '$1($3)',
    };

    // asin検索 //
    var patAsin = {
        '^http://(d|diary).hatena.ne.jp/asin(mobile|diary)?/([^/?]+).*': 'ASIN:$3',
        '^http://(www.)?1470.net/bm/asininfo/([0-9A-Z]+)': 'ASIN:$2',
        '^http://allconsuming.jp/item.cgi\\?asin=([0-9A-Z]+)': 'ASIN:$1',
    };

    // JAN/EAN検索 //
    var patEan = {
        '^http://(d|diary).hatena.ne.jp/ean/([^/?]+).*': 'EAN:$2',
    };

    // privateなリンク //
    var patPrivate = {
        '^http://127.0.0.1(:[0-9]+)?/(.*)': 'localhost($2)',
        '^http://localhost(:[0-9]+)?/(.*)': 'localhost($2)',
        '^http://192.168.[0-9]+.[0-9]+(:[0-9]+)?/(.*)': 'local IP($2)',
        '^http://(www.)?bloglines.com/myblogs_display\\?(.*)': 'bloglines($2)',
        '^http://www.google.com/reader(/.*)': 'Google Reader($1)',
        '^http://mail.google.com/mail/(.*)': 'Gmail($1)',
        '^http://reader.livedoor.com(/.*)': 'LDR($1)',
    };

    // その他のリンク //
    var patOther = {
    };

    var replacePatternList = [
        { category: 'Asin',             pattern: patAsin,      },
        { category: 'EAN',              pattern: patEan        },
        { category: 'Keyword',          pattern: patKeywords,  },
        { category: 'Diary',            pattern: patDiaries,   },
        { category: 'Antenna',          pattern: patAntennae,  },
        { category: 'Bookmark',         pattern: patBookmarks, },
        { category: 'RSS',              pattern: patRSS,       },
        { category: 'Idea',             pattern: patIdeas,     },
        { category: 'Search',           pattern: patSearches,  },
        { category: 'Others(Hatena)',   pattern: patHatena,    },
        { category: 'Others',           pattern: patOther,     },
        { category: 'Private',          pattern: patPrivate,   },
    ];

    /*===== 設定終わり =====*/

    Object.prototype.keys = function() {
        var arrKeys = new Array();
        for (var key in this) {
            if (this.hasOwnProperty(key))
                arrKeys.push(key);
        }
        return arrKeys;
    }

    function makeIteratorFromList(ref_func, iter_num, offset) {
        if (typeof offset == 'undefined') offset = 0;
        var idx = offset;
        var idx_end = offset + iter_num;
        return function() {
            return (idx < idx_end) ? ref_func(idx++) : null;
        }
    }

    function makeArrayFromIterator(it_func) {
        var ret = new Array();
        var obj;
        while (obj = it_func()) {
            ret.push(obj);
        }
        return ret;
    }

    // ref: http://piro.sakura.ne.jp/xul/tips/x0032.html
    // ref: http://lowreal.net/logs/2006/03/16/1
    function createNSResolver(evaluator, nodeResolver) {
        const XULNS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
        const XHTMLNS = 'http://www.w3.org/1999/xhtml';
        const XLinkNS = 'http://www.w3.org/1999/xlink';
        const XMLNS = 'http://www.w3.org/XML/1998/namespace';

        var defaultRresolver = evaluator.createNSResolver(nodeResolver);
        var resolver = {
            defaultNS: null,
            defaultPrefix: '',
            lookupNamespaceURI : function(aPrefix) {
                switch (aPrefix) {
                    case 'xul':
                        return XULNS;
                    case 'xml':
                        return XMLNS;
                    case 'html':
                    case 'xhtml':
                        return XHTMLNS;
                    case 'xlink':
                        return XLinkNS;
                    default:
                        // return '';
                        return (this.defaultNS != null) ?
                           this.defaultNS :
                           defaultResolver.lookupNamespaceURI(this.defaultPrefix);
                }
            }
        };

        return resolver;
    }

    // ref: http://piro.sakura.ne.jp/xul/tips/x0032.html
    function getNodesFromXPath(aXPath, aContextNode, aType) {
        const type = aType || XPathResult.ANY_TYPE;
        const xmlDoc  = aContextNode ? aContextNode.ownerDocument : document ;
        const context = aContextNode || xmlDoc.documentElement;

        var resolver = createNSResolver(xmlDoc, context);
        var expression = xmlDoc.createExpression(aXPath, resolver);
        return expression.evaluate(context, type, null);
    }

    // ref: http://lowreal.net/logs/2006/03/13/3
    // ref: http://lowreal.net/logs/2006/03/16/1
    function $X(exp, context, type) {
        // type = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE;
        // type = XPathResult.ORDERED_NODE_ITERATOR_TYPE;
        // var singleValueWrapper = function(val) { return (type != null) ? val : [ val ] };
        var result = getNodesFromXPath(exp, context, type);
        switch (result.resultType) {
            case XPathResult.NUMBER_TYPE: return result.numberValue;
            case XPathResult.STRING_TYPE: return result.stringValue;
            case XPathResult.BOOLEAN_TYPE: return result.booleanValue;

            case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
            case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
                var it = function() { return result.iterateNext(); };
                return makeArrayFromIterator(it);

            case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
            case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
                var it = makeIteratorFromList(function(i) { return result.snapshotItem(i) },
                                              result.snapshotLength);
                return makeArrayFromIterator(it);

            default:
                throw 'ERROR: unknown resultType';
        }
    }

    function replaceByPattern(replacePatternList, text) {
        for (var pat_idx = 0, pat_len = replacePatternList.length; pat_idx < pat_len; pat_idx++) {
            var cat = replacePatternList[pat_idx].category;
            var pattern_list = replacePatternList[pat_idx].pattern;
            for (var patFrom in pattern_list) {
                if (! pattern_list.hasOwnProperty(patFrom)) continue;
                var patTo = pattern_list[patFrom];
                var text2 = text.replace(RegExp(patFrom), patTo);
                if (text != text2) {
                    text = text2;
                    // text = text2.replace(/\+/g, ' ');
                    category = cat;
                    return { category: cat, text: text };
                }
            }
        }
        return { category: 'Others', text: text };
    }

    var divReferList = $X('//div[@id="referer-list"]')[0];
    if (!divReferList) return;

    // refererItemHref[アンカーテキスト] = [リンク先1, リンク先2, ...]
    var refererItemHref  = new Object();

    // refererItemCount[カテゴリ][アンカーテキスト]
    var refererItemCount = new Object();

    function registerRefererItem(category, text, href) {
        if (!refererItemCount[category])
            refererItemCount[category] = new Object();
        if (!refererItemCount[category][text])
            refererItemCount[category][text] = 0;

        refererItemCount[category][text] += refererCount;

        if (!refererItemHref[text])
            refererItemHref[text] = new Array();
        refererItemHref[text].push(href);
    }

    function addCategory(divReferList, cat) {
        var aAssoc = refererItemCount[cat];
        var keys = aAssoc.keys().sort(function(a, b) {
                var cmp = aAssoc[b] - aAssoc[a];
                if (cmp == 0) {
                    if (a < b) cmp = -1;
                    else if (a > b) cmp = 1;
                }
                return cmp;
            });

        var h4 = document.createElement('h4');
        h4.appendChild(document.createTextNode(cat));
        divReferList.appendChild(h4);

        var ulRefCategory = document.createElement('ul');
        for (var k = 0; k < keys.length; k++) {
            var key = keys[k];

            var liItem = document.createElement('li');

            var text = document.createTextNode(refererItemCount[cat][key] + ' ');
            liItem.appendChild(text);

            var a = document.createElement('a');
            a.href = refererItemHref[key][0];
            a.textContent = key;
            liItem.appendChild(a);

            for (var i = 1; i < refererItemHref[key].length; i++) {
                var text = document.createTextNode(' ');
                liItem.appendChild(text);
                var a = document.createElement('a');
                a.href = a.title = refererItemHref[key][i];
                a.textContent = i;
                liItem.appendChild(a);
            }
            ulRefCategory.appendChild(liItem);
        }
        divReferList.appendChild(ulRefCategory);

        return ulRefCategory;
    }

    // リファラ情報を集める
    var liRefererItems = $X('./ul/li', divReferList);
    if (liRefererItems.length == 0) return;
    for (var li_idx = 0, li_len = liRefererItems.length; li_idx < li_len; li_idx++) {
        var refererCount;
        if (liRefererItems[li_idx].textContent.match(/^\s*(\d+)\s+/))
            refererCount = parseInt(RegExp.$1);
        if (!refererCount) {
            GM_log('refererCount is null');
            continue;
        }

        // リファラへのアンカー
        var aRefererItem = $X('./a', liRefererItems[li_idx])[0];
        if (!aRefererItem) {
            // GM_log('aRefererItem is null');
            continue;
        }

        var replaceResult = replaceByPattern(replacePatternList, aRefererItem.text);
        var text = replaceResult.text;
        var category = replaceResult.category;

        registerRefererItem(category, text, aRefererItem.href);
    }

    // リンク元リストを削除
    var ulRefererItems = $X('./ul', divReferList);
    if (!ulRefererItems) return;
    for (var ul_idx = 0, ul_len = ulRefererItems.length; ul_idx < ul_len; ul_idx++) {
        divReferList.removeChild(ulRefererItems[ul_idx]);
    }

    // 「リンク元を削除する」ボタンを削除
    var deleteReferer = $X('//input[@name = "deletereferer"]')[0];
    if (deleteReferer) {
        deleteReferer.parentNode.removeChild(deleteReferer);
    }

    // カテゴリ毎にリンク元リストを追加
    var ulList = new Object();
    for (var cat_idx = 0, cat_len = replacePatternList.length; cat_idx < cat_len; cat_idx++) {
        var cat = replacePatternList[cat_idx].category;
        if (refererItemCount[cat])
            ulList[cat] = addCategory(divReferList, cat);
    }

    /*===== リンク元のページタイトルを追加する処理 =====*/

    function TitleUpdater(ext) {
        for (var prop in ext) {
            if (ext.hasOwnProperty(prop)) {
                this[prop] = ext[prop];
            }
        }
    };

    TitleUpdater.prototype = {
        getUrlFromAnchor: function(anchor) { return null; },
        getTitleFromText: function(text) { return null; },
        update: function (ulList) {
            var links = $X('.//li//a[position()=1]', ulList);
            for (var i = 0; i < links.length; i++) {
                var anchor = links[i];
                var url = this.getUrlFromAnchor(anchor);
                if (url)
                    this.setTitleAsync(anchor, url);
            }
        },
        setTitleAsync: function(anchor, url) {
            const getTitleFromText = this.getTitleFromText;
            GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    onload: function(responseDetails) {
                        var title = getTitleFromText(responseDetails.responseText);
                        if (title) {
                            var text = document.createTextNode('');
                            text.textContent = ' - ' + title;
                            anchor.parentNode.appendChild(text);
                        }
                    }
                });
        },
    };

    if (ulList['Asin']) {
        var titleUpdater = new TitleUpdater({
            getUrlFromAnchor: function(anchor) {
                if (anchor.textContent.match(/ASIN:(.*)/)) {
                    var asin = RegExp.$1;
                    return 'http://d.hatena.ne.jp/asin/' + asin;
                }
                return null;
            },
            getTitleFromText: function(text) {
                if (text.match(/<h1>(.*)<\/h1>/)) {
                    return RegExp.$1;
                }
                return null;
            },
        });

        titleUpdater.update(ulList['Asin']);
    }

    if (ulList['EAN']) {
        var titleUpdater = new TitleUpdater({
            getUrlFromAnchor: function(anchor) {
                if (anchor.textContent.match(/EAN:(.*)/)) {
                    var ean = RegExp.$1;
                    return 'http://d.hatena.ne.jp/ean/' + ean;
                }
                return null;
            },
            getTitleFromText: function(text) {
                if (text.match(/<h1>(.*)<\/h1>/)) {
                    return RegExp.$1;
                }
                return null;
            },
        });

        titleUpdater.update(ulList['EAN']);
    }

    if (ulList['Search'] || ulList['Bookmark']) {
        var titleUpdater = new TitleUpdater({
            getUrlFromAnchor: function(anchor) {
                if (anchor.textContent.match(/(http:\/\/[^\)]+)/)) {
                    // var url = RegExp.$1;
                    var url = anchor.href;
                    return url;
                }
                return null;
            },
            getTitleFromText: function(text) {
                if (text.match(/<title>([^<]+)<\/title>/)) {
                    return RegExp.$1;
                }
                return null;
            },
        });

        if (ulList['Search'])
            titleUpdater.update(ulList['Search']);
        if (ulList['Bookmark'])
            titleUpdater.update(ulList['Bookmark']);
    }
})();