var PLUGIN_INFO = {NAME} Hatena Bookmark UserSearch はてなブックマークユーザ検索 2.0pre 2.0pre http://svn.coderepos.org/share/lang/javascript/vimperator-plugins/trunk/hatena-bookmark-search.js Yuichi Tateno MPL 1.1/GPL 2.0/LGPL 2.1 1.0.2 || :bs[earch][!] word :tabbs[earch][!] word ||< ログインしているユーザのブックマークを、URL, コメント, タイトル から検索します。 はてなブックマークユーザページの右上の検索のローカル版のようなイメージです。 XUL/Migemo が入っている場合は Migemo を使い正規表現検索します。 Migemo を利用した検索語の絞り込みはスペース区切りの 2単語までとなります。 Migemo を利用すると検索が重くなるので、遅いマシンやインクリメンタル検索環境下では、以下の設定をすることで migemo 検索をしなくなります。 >|| let g:hatena_bookmark_no_migemo='true'; ||< また >|| let g:hatena_bookmark_suffix_array='true'; ||< とすることで、SuffixArray での検索を有効にします。現在は SuffixArray の構築に時間がかかるため、10000件ぐらいまでのブックマークでないと実用的ではありません。SuffixArray を利用すると、検索のコストが10000件ぐらいでは 1,2ms ぐらいになるとおもいます。また migemo 検索はできません。 :bs word では、選択している URL を開きます。:bs! word では、選択している URL のはてなブックマークエントリーページを開きます。:bs と単語を入力しないと、http://b.hatena.ne.jp/my を開きます。:bs! では http://b.hatena.ne.jp/ トップページを開きます。 初回検索時にデータを構築しますが、強制的にデータをロードし直したい時などは >|| :bs -reload ||< としてください。 (invalid options エラーが出る場合は、適当な文字を後ろに付加するか、最新(Nightly)の Vimperator を使ってください) また、:open, :tabopen の補完で、completeオプションに "H" を追加することではてなブックマークの検索が可能です。 >|| :set complete+=H ||< .vimperatorrcに書く場合は >|| autocmd VimperatorEnter .* :set complete+=H ||< としてください。 == ChangeLog == - 1.0.2 -- ヌル文字を消す - 1.0.1 -- ドキュメントの追加 - 1.0.0 -- キャッシュの追加, SuffixArray 検索の追加 ]]> ; liberator.plugins.HatenaBookmark = (function(){ let p = function(arg) { Application.console.log(''+arg); // liberator.log(arg); } p.b = function(func, name) { let now = (new Date() * 1); func(); let t = (new Date() * 1) - now; // p('sary: ' + name + ': ' + t); } const HatenaBookmark = {}; HatenaBookmark.Data = new Struct('data'); /* * title * comment * url */ HatenaBookmark.Data.prototype.__defineGetter__('title', function() this.data.split("\n")[0].replace("\0", '')); HatenaBookmark.Data.prototype.__defineGetter__('comment', function() this.data.split("\n")[1]); HatenaBookmark.Data.prototype.__defineGetter__('url', function() this.data.split("\n")[2]); HatenaBookmark.Data.prototype.__defineGetter__('icon', function() bookmarks.getFavicon(this.url)); HatenaBookmark.Data.prototype.__defineGetter__("extra", function () [ ["comment", this.comment, "Comment"], ].filter(function (item) item[1])); var XMigemoCore; var XMigemoTextUtils; try { XMigemoCore = Cc['@piro.sakura.ne.jp/xmigemo/factory;1'] .getService(Ci.pIXMigemoFactory) .getService("ja"); XMigemoTextUtils = Cc['@piro.sakura.ne.jp/xmigemo/text-utility;1'].getService(Ci.pIXMigemoTextUtils); } catch (e if e instanceof TypeError) { } HatenaBookmark.useSuffixArray = !!(liberator.globalVariables.hatena_bookmark_suffix_array); HatenaBookmark.useMigemo = !!(!liberator.globalVariables.hatena_bookmark_no_migemo && XMigemoCore); HatenaBookmark.reload = function() { if (HatenaBookmark.useSuffixArray) { HatenaBookmark.SuffixArray.reload(); } else { HatenaBookmark.UserData.reload(); } } HatenaBookmark.Command = { templateDescription: function (item, text) { return <> { !(item.extra && item.extra.length) ? "" : { template.map(item.extra, function (e) <>{e[1]}, <> /* Non-breaking space */) } } }, templateTitleIcon: function (item, text) { var simpleURL = text.replace(/^https?:\/\//, ''); if (simpleURL.indexOf('/') == simpleURL.length-1) simpleURL = simpleURL.replace('/', ''); return <>{item.icon ? : <>}{item.item.title} { simpleURL } }, filter: function (_item) { var item = _item.item; // 'this' is context object. if (HatenaBookmark.useMigemo) { if (!this.migemo) { this.migemo = HatenaBookmark.Command.compileRegexp(this.filter); } var migemo = this.migemo; return migemo.test(item.data); } else { return this.match(item.url) || this.match(item.comment) || this.match(item.title); } }, compileRegexp: function(str) { let a; with (XMigemoTextUtils) { a = sanitize(trim(str)).split(/\s+/).join(' '); } return new RegExp(XMigemoTextUtils.getANDFindRegExpFromTerms(XMigemoCore.getRegExps(a)), 'gim'); }, execute: function(args) { if (args['-reload']) { HatenaBookmark.reload(); liberator.echo('HatenaBookmark data reloaded.'); return; } var url = HatenaBookmark.Command.genURL(args); liberator.open(url); }, executeTab: function(args) { var url = HatenaBookmark.Command.genURL(args); liberator.open(url, liberator.NEW_TAB); }, genURL: function(args) { var url = (args.string || '').replace(/\s/g, ''); if (url.length) { if (args.bang) { return 'http://b.hatena.ne.jp/entry/' + url.replace('#', '%23'); } else { return url; } } else { if (args.bang) { return 'http://b.hatena.ne.jp/'; } else { return 'http://b.hatena.ne.jp/my'; } } }, createCompleter: function(titles) { return function(context) { context.format = { anchored: true, title: titles, keys: { text: "url", description: "url", icon: "icon", extra: "extra"}, process: [ HatenaBookmark.Command.templateTitleIcon, HatenaBookmark.Command.templateDescription, ], } context.ignoreCase = true; if (HatenaBookmark.useSuffixArray) { context.filters = []; context.completions = HatenaBookmark.SuffixArray.search(context.filter); } else { if (context.migemo) delete context.migemo; context.filters = [HatenaBookmark.Command.filter]; context.completions = HatenaBookmark.UserData.bookmarks; } } } } HatenaBookmark.Command.options = { completer: HatenaBookmark.Command.createCompleter(['TITLE', 'Info']), literal: 0, argCount: '*', bang: true, options: [ [['-reload'], commands.OPTION_NOARG] ], } commands.addUserCommand( ['bs[earch]'], 'Hatena Bookmark UserSearch', HatenaBookmark.Command.execute, HatenaBookmark.Command.options, true ); commands.addUserCommand( ['tabbs[earch]'], 'Hatena Bookmark UserSearch', HatenaBookmark.Command.executeTab, HatenaBookmark.Command.options, true ); completion.addUrlCompleter("H", "Hatena Bookmarks", HatenaBookmark.Command.createCompleter(["Hatena Bookmarks"])); HatenaBookmark.Cache = { get store() { if (!this._store) { let key = 'plugins-hatena-bookmark-search-data'; this._store = storage.newMap(key, true); } return this._store; }, get now() { return (new Date * 1); }, clear : function () { let store = this.store; store.remove('expire'); store.remove('data'); store.remove('saryindexes'); }, get data () { let store = this.store; let expire = store.get('expire'); if (expire && expire > this.now) { return store.get('data'); } else { return this.loadByRemote(); } }, get expire() { // 24 hours; return this.now + (liberator.globalVariables.hatena_bookmark_cache_expire || 1000 * 60 * 24); }, loadByRemote: function() { let r = util.httpGet('http://b.hatena.ne.jp/my.name'); let check = eval('(' + r.responseText + ')'); if (!check.login) { liberator.echo('please login hatena bookmark && :bsearch -reload '); this.store.set('expire', this.expire); this.store.set('data', ''); return ''; } else { let url = 'http://b.hatena.ne.jp/my/search.data'; let res = util.httpGet(url); this.store.set('expire', this.expire); this.store.set('data', res.responseText); return res.responseText; } }, get sary() { let data = this.data; if (data[0] != "\0") { data = data.substr(0, data.length * 3/4).split("\n").map(function(s, i) (i % 3 == 0) ? ("
/*** BEGIN LICENSE BLOCK {{{
    Copyright (c) 2008 suVene<suvene@zeromemory.info>

    distributable under the terms of an MIT-style license.
    http://www.opensource.jp/licenses/mit-license.html
}}}  END LICENSE BLOCK ***/
// PLUGIN_INFO//{{{
var PLUGIN_INFO =
<VimperatorPlugin>
    <name>{NAME}</name>
    <description>notification from the subjects is notified to you by the Growl style.</description>
    <description lang="ja">Growl風通知</description>
    <author mail="suvene@zeromemory.info" homepage="http://zeromemory.sblo.jp/">suVene</author>
    <version>0.1.7</version>
    <license>MIT</license>
    <minVersion>2.0pre</minVersion>
    <maxVersion>2.0pre</maxVersion>
    <updateURL>http://svn.coderepos.org/share/lang/javascript/vimperator-plugins/trunk/notifier/observer_growl.js</updateURL>
    <detail><![CDATA[
== Settings ==
>||
liberator.globalVariables.observer_growl_settings = {
    'message title': {
        life: number,           // sec (10 sec by default)
        sticky: bool,           // true or false (false by default)
        sticky_keywords: [      // keyword ary
            'keyword1',
            'keyword2'
        ],
        hide: bool              // true or false (false by default)
                                //   however it's displayed when there is a keyword in the message.
    }
};
||<
e.g.)
>||
javascript <<EOM
liberator.globalVariables.observer_growl_settings = {
    'Hatelabo bottle': { life: 20, sticky_keywords: ['はてな'] },
    'Weather forecast by Yahoo!': { sticky: true }
};
EOM
||<

== ToDo ==
- hide
    ]]></detail>
</VimperatorPlugin>;
//}}}
(function() {

var notifier = liberator.plugins.notifier;
if (!notifier) return;

var libly = notifier.libly;
var $U = libly.$U;
var logger = $U.getLogger('observer_growl');

var Growl = function() {//{{{
    this.initialize.apply(this, arguments);
};
Growl.prototype = {
    initialize: function(node, options, message) {
        this.defaults = {
            life: 10,
            sticky: false,
            sticky_keywords: [],
            hide: false
        };
        this.node = node;
        this.created = new Date();
        this.options = $U.extend(this.defaults, (options || {}));
        this.sticky_keywords_exp =
            this.options.sticky_keywords.map(function(k) new RegExp(k, 'i'));
        this.message = message;
        var div = node.getElementsByTagName('div');
        div[0].addEventListener('click', $U.bind(this, this.remove), false);
    },
    remove: function() {
        // TODO: animation!!!!
        this.node.parentNode.removeChild(this.node);
    },

};//}}}

notifier.observer.register(notifier.Observer, {
    initialize: function () {
        this.count = 1;
        this.settings;
        this.intervalIDs = {};

        io.getRuntimeDirectories('').forEach(function(dir) {
            var path = io.expandPath(dir.path + '/plugin/notifier');
            $U.readDirectory(path, '^growl', function(f) {
                try {
                    io.source(f.path, true);
                    logger.log('load success: ' + f.leafName);
                } catch (e) {
                    logger.log('load failed: ' + f.leafName);
                }
            });
        });

        this.settings = liberator.globalVariables.observer_growl_settings || {};
    },
    update: function(message) {

        var doc = window.content.document;
        var container = doc.getElementById('observer_growl');
        if (!container) {
            doc.body.appendChild($U.xmlToDom(<div id="observer_growl" class="observer_growl top-right"/>, doc));
            container = doc.getElementById('observer_growl');
            window.content.addEventListener('unload', $U.bind(this, function() {
                if (container.__interval__) {
                    clearInterval(container.__interval__);
                    this.intervalIDs[container.__interval__] = false;
                }
            }), false);
        }
        var closer = doc.getElementById('observer_growl_closer');

        var notification = this.createPopup(message, doc, container);
        // TODO: animation!!!
        //container.appendChild(doc.importNode(notification, true));
        container.appendChild(notification);

        if (container.childNodes.length == 1 && !container.__interval__) {
            let interval = setInterval($U.bind(this, this.checkStatus), 1000);
            this.intervalIDs[interval] = true;
            container.__interval__ = interval;
        } else if (container.childNodes.length >= 2) {
            if (!closer) {
                closer = $U.xmlToDom(<div id="observer_growl_closer" class="observer_growl_closer center" style="display: block;">[close all]</div>, doc);
                container.insertBefore(closer, container.firstChild);
                closer.addEventListener('click', $U.bind(this, this.removeAll, 'test'), false);
            }
        }

        this.count++;
    },
    createPopup: function(message, doc, container) {
        var node;
        var html =
            <div class="observer_growl_notification" style="display: block;">
                <div class="close">&#215;</div>
                <div class="header">{new XMLList(
                    (message.link ? '<a href="' + message.link + '">' : '') +
                    this.count + ': ' + message.title +
                    (message.link ? '</a>' : '')
                    )}</div>
                <div class="message">{new XMLList(message.message || '')}</div>
            </div>;
        node = $U.xmlToDom(html, doc);
        node.__data__ = new Growl(node, this.settings[message.title], message);
        return node;
    },
    checkStatus: function(force) {
        force = force == 'EVENT_REMOVE_ALL';

        var doc = window.content.document;
        var container = doc.getElementById('observer_growl');
        if (!container) return;

        var removeNodes = [];
        for (let i = 0, len = container.childNodes.length; i < len; i++) {
            let item = container.childNodes[i];
            let growl = item.__data__;

            if (item.id == 'observer_growl_closer') {
                if (len == 1)
                    item.parentNode.removeChild(item);
                continue;
            }

            if (force || this.canRemove(growl))
                removeNodes.push(item);
        }
        removeNodes.forEach(function(element) element.__data__.remove());

        if (force || container.childNodes.length == 0) {
            clearInterval(container.__interval__);
            container.__interval__ = 0;
        }

    },
    canRemove: function(growl) {
        if (!growl || !growl.created || growl.options.sticky) return false;

        var text = growl.message.title + ' ' +
                   growl.message.message.replace(/(?:<[^>]*>)+/g, '');
        if (growl.sticky_keywords_exp.some(function(k) k.test(text)) ||
            growl.created.getTime() + growl.options.life * 1000 > new Date().getTime())
            return false;

        return true;
    },
    removeAll: function(a) {
        this.checkStatus('EVENT_REMOVE_ALL');
        var closer = window.content.document.getElementById('observer_growl_closer');
        if (closer)
            closer.parentNode.removeChild(closer);
    },
    shutdown: function() {
        for (let [id, flg] in Iterator(this.intervalIDs)) {
            if (!flg) return;
            clearInterval(id);
            this.intervalIDs[id] = false;
        }
    }
});

})();
// vim: set fdm=marker sw=4 ts=4 sts=0 et: