hatebuize

By janus_wel Last update Sep 20, 2008 — Installed 67 times. Daily Installs: 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           hatebuize
// @namespace      http://d.hatena.ne.jp/janus_wel/
// @description    add bookmarked number of はてなブックマーク to specified position by SITEINFO
// ==/UserScript==
/*
 * VERSION
 *  1.00
 *
 * LICENSE
 *  New BSD License
 *
 * ACKNOWLEDGMENT
 *  this script based on 'del.icio.us meets Hatena Bookmark',
 *  AutoPagerize and many others !!
 *
 *  refer:
 *      - http://la.ma.la/blog/diary_200512131313.htm
 *      - http://d.hatena.ne.jp/m4i/20051213/1134425307
 *      - http://userscripts.org/scripts/show/8551
 *
 * HISTORY
 *  2008/09/15  ver. 0.10   - initial written.
 *  2008/09/16  ver. 0.11   - refactoring.
 *  2008/09/17  ver. 0.20   - add command that toggle AutoPagerize
 *                            information display to Greasemonkey menu.
 *                          - add receiver for 'GMHatebuizeToggle'
 *                            CommandEvent that toggle AutoPagerize
 *                            information display.
 *  2008/09/19  ver. 1.00   - work togather with wedata.net.
 *                          - add command that clear SITEINFO cache.
 *                          - add receiver for 'GMHatebuizeClearCache'
 *                            CommandEvent that clear SITEINFO cache.
 *                          - refactoring.
 * */

(function() {

// constant variables ---------------------------------------------------
// for development
const DEBUG = false;

// this definitions are given priority in SITEINFO application
const SITEINFO = [
/* template
    {
        name:           '',
        applyURL:       '',
        urlNode:        '',
        insertPosition: '',
        after:          false,
    },
*/
];


// extention of native class --------------------------------------------
// extend the feature that bind the Function to the Object
// NOTICE: this function is return the Function object
// refer: bind function in prototype.js
//        http://www.prototypejs.org/
Function.prototype.bind = function(object) {
    var __method = this;
    return function() {
        return __method.apply(object, arguments);
    }
};


// class definition -----------------------------------------------------
// XML-RPC like GM_xmlhttpRequest
function XMLRPC() {
    this.initialize.apply(this, arguments);
}

XMLRPC.prototype = {
    constants: {
        CHUNK_SIZE_DEFAULT: 1,
    },

    initialize: function() {
        // manipulable properties from external by getter/setter
        this._url = '';
        this._chunkSize  = this.constants.CHUNK_SIZE_DEFAULT;
        this._method = '';
        this._onload = '';
        this._params = [];

        // propertis assumed that private
        this._chunks  = [];
        this._calledLoadHandlerCount = 0;
        this._numofChunk = 0;
        this._response;
    },

    // getter / setter for end point
    url: function(url) {
        if(url) {
            if(!isHttpURL(url)) {
                throw new Error('assign HTTP URL to url');
            }
            this._url = url;
            return this;
        }
        else {
            return this._url;
        }
    },

    // getter / setter for chunk size
    // amount of data ( = params) posted by GM_xmlhttpRequest
    chunkSize: function(chunkSize) {
        if(chunkSize) {
            if(!isNumber(chunkSize)) {
                throw new Error('assign number to chunkSize');
            }
            this._chunkSize = chunkSize;
            return this;
        }
        else {
            return this._chunkSize;
        }
    },

    // getter / setter for method
    method: function(method) {
        if(method) {
            // check data-type
            if(!isString(method)) {
                throw new Error('assign string to method');
            }
            this._method = method;
            return this;
        }
        else {
            return this._method;
        }
    },

    // getter / setter for params
    data: function(params) {
        if(params) {
            // check data-type
            if(!isArray(params)) {
                throw new Error('assign Array object to params');
            }

            this._params = Array.apply(null, params);
            return this;
        }
        return this._params;
    },

    // getter / setter for callback function execute on loading
    onload: function(onload) {
        if(onload) {
            if(!isFunction(onload)) {
                throw new Error('assign Function object to onload');
            }

            this._onload = onload;
            return this;
        }
        else {
            return this._onload;
        }
    },

    // send http request and handle response
    send: function() {
        // build up chunks that will be sended.
        this._buildChunks();

        for (var i=0, max=this._numofChunk ; i<max ; ++i) {
            GM_xmlhttpRequest({
                method: 'POST',
                url:    this._url,
                data:   this._chunks[i].toString(),
                onload: this._loadHandler.bind(this)
            });
        }

        return this;
    },

    // pre-process before send
    _buildChunks: function() {
        // calculate the number of chunks
        var numofParams = this._params.length;
        if(numofParams > 1) {
            this._numofChunk = Math.ceil(numofParams / this._chunkSize);
        }

        // build up !!
        var chunkSize = this._chunkSize;
        var method = this._method;
        var params = this._params;
        for(var i=0, max=this._numofChunk ; i<max ; ++i) {
            var chunk = this._chunkTemplate(method);
            var count = -1;
            for(;;) {
                var param = params.shift();
                if(++count === chunkSize || !param) break;
                chunk..params.appendChild(this._paramTemplate(param));
            }

            this._chunks.push(chunk);
        }

        return this;
    },

    // callback function for GM_xmlhttpRequest
    _loadHandler: function(res)
    {
        var response = new XML(res.responseText.replace(/^<\?xml.*?\?>/, ''));
        if (this._response) {
            this._response..struct.appendChild(response..member);
        }
        else {
            this._response = response;
        }

        if (++(this._calledLoadHandlerCount) === this._numofChunk) {
            this._onload(this._response);
        }
    },

    // chunk template
    _chunkTemplate: function(method) {
        if(isString(method)) {
            var chunkTemplate =
                <methodCall>
                    <methodName>{method}</methodName>
                    <params></params>
                </methodCall>
                ;
            return chunkTemplate;
        }
        return null;
    },

    // param template
    // now, string only
    // TODO implement each data-type
    _paramTemplate: function(param) {
        if(isString(param)) {
            var paramTemplate =
                <param>
                    <value>
                        <string>{param}</string>
                    </value>
                </param>
                ;
            return paramTemplate;
        }
        return null;
    },
};


// HatebuizeController class
// singleton
function HatebuizeController() {
    var self = arguments.callee;
    if(self.instance == null) {
        this.initialize.apply(this, arguments)
        self.instance = this;
    }
    return self.instance;
}

HatebuizeController.prototype = {
    initialize: function(siteinfos) {
        this.siteinfo = this._buildHatebuizeController(siteinfos);
        debug('SITEINFO: ' + this.siteinfo.toSource());

        this.insertPositions = {};
        this.urls = [];
        this.afterFlag = !!(this.siteinfo.after);
    },

    constants: {
        // for はてなブックマーク API
        // refer: http://d.hatena.ne.jp/keyword/%A4%CF%A4%C6%A4%CA%A5%D6%A5%C3%A5%AF%A5%DE%A1%BC%A5%AF%B7%EF%BF%F4%BC%E8%C6%C0API

        IDENTIFIER:     'gm_hatebuize',

        ENTRY_URI:      'http://b.hatena.ne.jp/entry/',
        END_POINT:      'http://b.hatena.ne.jp/xmlrpc',
        CHUNK_SIZE_MAX: 50,

        HATEBU_SINGLE_TEMPLATE: 'user',
        HATEBU_PLURAL_TEMPLATE: 'users',

        HOTTER_USER_COUNT:  5,
        HOTTEST_USER_COUNT: 10,

        HOT_STYLE: [
            'margin           : 0 3px;',
            'font-style       : normal !important;',
            'font-size        : small !important;',
            'text-decoration  : underline !important;',
        ].join(''),

        HOTTER_STYLE: [
            'margin           : 0 3px;',
            'font-style       : normal !important;',
            'font-size        : small !important;',
            'text-decoration  : underline !important;',
            'color            : #ff6666 !important;',
            'background-color : #fff0f0 !important;',
            'font-weight      : bold !important;',
        ].join(''),

        HOTTEST_STYLE: [
            'margin           : 0 3px;',
            'font-style       : normal !important;',
            'font-size        : small !important;',
            'text-decoration  : underline !important;',
            'color            : red !important;',
            'background-color : #ffcccc !important;',
            'font-weight      : bold !important;',
        ].join(''),

    },
    // build HatebuizeController object on appropriate SITEINFO
    _buildHatebuizeController: function(siteinfos) {
        for(var i=0, max=siteinfos.length ; i<max ; ++i) {
            var info = siteinfos[i];
            if(location.href.match(info.applyURL)) {
                return info;
            }
        }

        throw new Error('not found SITEINFO.');
    },

    // main method
    hatebuize: function(newNodes) {
        // initialize
        this.urls = [];
        this.insertPositions = {};

        if(newNodes) {
            // for AutoPagerize
            debug('newNodes.length: ' + newNodes.length);

            for(var i=0, max=newNodes.length ; i<max ; ++i) {
                this._pickoutBySiteinfo(newNodes[i]);
            }
        }
        else {
            this._pickoutBySiteinfo();
        }

        if(this.urls.length) {
            var xmlrpc = new XMLRPC;
            xmlrpc.chunkSize(this.constants.CHUNK_SIZE_MAX)
                  .url(this.constants.END_POINT)
                  .method('bookmark.getCount')
                  .data(this.urls)
                  .onload(this._callback.bind(this))
                  .send();
        }
    },

    // pickout URLs and nodes by XPath of SITEINFO
    _pickoutBySiteinfo: function(newNode) {
        // evaluate!!  results are ordered,
        // so url correspond to insert position (node)
        var hrefs = $s(this.siteinfo.urlNode, newNode);
        var insertPositions = $s(this.siteinfo.insertPosition, newNode);

        // cache results length
        var hrefsLength = hrefs.length;
        var insertPositionsLength = insertPositions.length;
        debug(hrefsLength);
        debug(insertPositionsLength);

        // if conflict both length, we can't make valid pairs
        if(! (hrefsLength === insertPositionsLength)) {
            throw new Error(
                [
                    'a number of results that evaluate XPath is conflict',
                    'number of urls: ' + hrefsLength,
                    'number of insert positions: ' + insertPositionsLength,
                ].join("\n")
            );
        }

        // process data
        for(var i=0 ; i<hrefsLength ; ++i) {
            // pickout URL
            var href = hrefs[i];
            var url = href.nodeValue;
            if(!url) {
                url = href.textContent;

                // fine-tuning
                if(! url.match(/^s?https?:\/\//)) url = 'http://' + url;
                if(url.match(/:\/\/[^\/]+$/)) url += '/';
            }

            // build the hash(object) that is key: url, value: node
            this.insertPositions[url] = insertPositions[i];
            // escape the key to object property
            this.urls.push(url);
        }
    },

    // callback for XMLRPC
    _callback: function(response) {
        debug(response);

        // pickout number of bookmarded
        var users = {};
        for each (var member in response..member) {
            users[member.name] = parseInt(member..int.toString(), 10);
        }

        for(var i=0, max=this.urls.length ; i<max ; ++i) {
            var url = this.urls[i];

            // if nobody bookmark the url, not display
            var numofUser = users[url];
            if(!numofUser) continue;

            // insert Hatebu node
            var insertPosition = this.insertPositions[url];
            var hatebuNode = this._buildHatebuNode(url, numofUser);

            this.afterFlag
                ? insertNodeAfterSpecified(hatebuNode, insertPosition)
                : insertNodeBeforeSpecified(hatebuNode, insertPosition);
        }
    },

    // red one
    _buildHatebuNode: function(url, numofUsers) {
        var a = document.createElement('a');
        a.setAttribute('class', this.constants.IDENTIFIER);
        a.setAttribute(
            'href',
            this.constants.ENTRY_URI + url.replace(/#/, '%23')
        );
        a.setAttribute(
            'style',
            numofUsers >= this.constants.HOTTEST_USER_COUNT
                ? this.constants.HOTTEST_STYLE
                : numofUsers >= this.constants.HOTTER_USER_COUNT
                    ? this.constants.HOTTER_STYLE
                    : this.constants.HOT_STYLE
        );

        var text = document.createTextNode(
            numofUsers + ' ' +
            (numofUsers === 1
                ? this.constants.HATEBU_SINGLE_TEMPLATE
                : this.constants.HATEBU_PLURAL_TEMPLATE)
        );

        a.appendChild(text);
        return a;
    },
};

// SITEINFO controller class
function SITEINFOController() {
    this._initialize.apply(this, arguments);
}

SITEINFOController.prototype = {
    _initialize: function () {
        GM_registerMenuCommand(
            'hatebuize - clear cache',
            this.clearCache.bind(this)
        );

        window.content.addEventListener(
            'GMHatebuizeClearCache',
            this.clearCache.bind(this),
            false
        );

        this.siteinfos;
    },

    _constants: {
        SITEINFO_IMPORT_URL: 'http://wedata.net/databases/hatebuize/items.json',
        CACHE_NAME: 'siteinfo',

        // 1 day, unit: msec
        EXPIRE_TIME: 60 * 60 * 24 * 1000,
    },

    // this script's ignition
    // setup SITEINFO and launch hatebuize
    setupSITEINFO: function() {
        // read cache
        var cache = this.readCache();

        debug('setupSITEINFO: ' + cache.toSource());

        // if there is no cache, import from wedata.net
        if (!cache || !(cache.lastExpire)) {
            this._importSITEINFO();
        }

        // expire ?
        var present = new Date().getTime();
        var lastExpire =  cache.lastExpire.getTime();
        if (present - lastExpire > this._constants.EXPIRE_TIME) {
            // in expiration
            this._importSITEINFO();
        }

        // ok, launch hatebuize
        launchHatebuize(SITEINFO.concat(cache.data));
    },

    // import SITEINFO and launch hatebuize
    _importSITEINFO: function () {
        debug('_importSITEINFO');
        GM_xmlhttpRequest({
            method:     'GET',
            url:        this._constants.SITEINFO_IMPORT_URL,
            onload:     this._onloadHandler.bind(this),
            onerror:    this._onerrorHandler.bind(this),
        });
    },

    // callback functions for GM_xmlhttpRequest
    _onloadHandler: function (response) {
        debug(response.status);

        // error handling
        if (response.status !== 200) {
            this._onerrorHandler();
        }

        // form data from wedata.net
        var wedata = eval(response.responseText).map(
            function (i) { return i.data; }
        );
        // make and write cache
        var cache = {
            lastExpire: new Date(),
            data:       wedata.sort(function (a, b) {
                            return a.applyURL.length - b.applyURL.length;
                        }),
        };
        this.writeCache(cache);

        // launch hatebuize
        launchHatebuize(SITEINFO.concat(wedata));
    },

    _onerrorHandler: function() {
        GM_log('can\'t import SITEINFO form wedata.net');
        launchHatebuize(SITEINFO);
    },

    // cache controller
    readCache: function () {
        return eval(GM_getValue(this._constants.CACHE_NAME, '')) || {};
    },

    writeCache: function (cacheObject) {
        GM_setValue(this._constants.CACHE_NAME, cacheObject.toSource());
    },

    clearCache: function () {
        GM_log('clear hatebuize\'s SITEINFO cache');
        GM_setValue(this._constants.CACHE_NAME, '');
    },
};

// AutoPagerizeDisplayController
function APInfoDisplayController() {
    this._initialize.apply(this, arguments);
};

APInfoDisplayController.prototype = {
    _initialize: function () {
        this.flag = GM_getValue('displayAutoPagerizeInfoFlag', false);

        // regist Greasemonkey menu command
        GM_registerMenuCommand(
            'hatebuize - toggle AutoPagerize info display',
            this.toggle.bind(this)
        );

        // litener for CommandEvent
        window.content.addEventListener(
            'GMHatebuizeToggle',
            this.toggle.bind(this),
            false
        );
    },

    toggle: function () {
        this.flag = !(this.flag);
        GM_setValue('displayAutoPagerizeInfoFlag', this.flag);
        GM_log(
            'AutoPagerize\'s SITEINFO display is '
            + (this.flag ? 'enable' : 'disable')
        );
    },
    is: function () {
        return this.flag;
    },

    // callback function for AutoPagerize.addDocumentFilter
    displayAPInfo: function (newDocument, requestURL, siteinfo) {
        if(this.is()) {
            var str = ['AutoPagerize SITEINFO'];
            for(var property in siteinfo) {
                str.push(property + ': ' + siteinfo[property]);
            }
            GM_log(str.join("\n"));
        }
    },
};


// function definitions -------------------------------------------------
function launchHatebuize(siteinfos) {
    try {
        var hc = new HatebuizeController(siteinfos);
        hc.hatebuize();

        var daic = new APInfoDisplayController();

        // regist callback function to AutoPagerize
        addFilterToAP( hc.hatebuize.bind(hc) );
        addDocumentFilterToAP( daic.displayAPInfo.bind(daic) );
    }
    catch(e) {
        debug(e.message);
    }
}


// main -----------------------------------------------------------------
var s = new SITEINFOController();
s.setupSITEINFO();


// stuff functions ------------------------------------------------------
// for AutoPagerize
function addFilterToAP(filterFunction) {
    if(window.AutoPagerize && window.AutoPagerize.addFilter) {
        setTimeout( function() {
            window.AutoPagerize.addFilter(filterFunction);
        }, 0, this.unsafeWindow || window);
    }
}

function addDocumentFilterToAP(filterFunction) {
    if(window.AutoPagerize && window.AutoPagerize.addDocumentFilter) {
        setTimeout( function() {
            window.AutoPagerize.addDocumentFilter(filterFunction);
        }, 0, this.unsafeWindow || window);
    }
}

// XPath (element.evaluate wrapper)
function $f(query, node) {
    if(!node) node = document;
    var result = (node.ownerDocument || node).evaluate(
        query,
        node,
        null,
        XPathResult.FIRST_ORDERED_NODE_TYPE,
        null
    );
    return result.singleNodeValue
        ? result.singleNodeValue
        : null;
}

function $s(query, node) {
    if(!node) node = document;
    var result = (node.ownerDocument || node).evaluate(
        query,
        node,
        null,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );
    var nodes = [];
    for(var i=0, max=result.snapshotLength ; i<max ; ++i)
        nodes.push(result.snapshotItem(i));
    return nodes;
}

// node control
function insertNodeBeforeSpecified(inserted, specified) {
    return specified.parentNode.insertBefore(inserted, specified);
}
function insertNodeAfterSpecified(inserted, specified) {
    var next = specified.nextSibling;
    if(next) {
        return specified.parentNode.insertBefore(inserted, next);
    }
    else {
        return specified.parentNode.appendChild(inserted);
    }
}

// utilities
function isString(something) {
    return (typeof something === 'string'
        || something.constructor === String);
}
function isNumber(something) {
    return (typeof something === 'number'
        || something.constructor === Number);
}
function isArray(something) {
    return (something.constructor === Array);
}
function isFunction(something) {
    return (typeof something === 'function'
        || something.constructor === Function);
}
function isHttpURL(something) {
    // refer: http://www.din.or.jp/~ohzaki/perl.htm#httpURL
    return (isString(something)
        && something.match(/s?https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+/g));
}

function debug(message) {
    if(DEBUG) {
        GM_log(message);
    }
}

})()

// vim: set sw=4 ts=4 et;