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) ? ("\0" + s) : s ).join("\n"); this.store.set('expire', this.expire); this.store.set('data', data); } let sary = new SuffixArray(data); let saryindexes = this.store.get('saryindexes'); if (saryindexes) { sary.sary = saryindexes.split(','); } else { sary.make(); this.store.set('saryindexes', sary.sary.join(',')); } return sary; }, } HatenaBookmark.SuffixArray = { get cache() HatenaBookmark.Cache, reload: function() { this.cache.clear(); this.sary = null; }, search: function(word) { if (word.length < 2) return []; if (!this.sary) { this.sary = this.cache.sary; } let sary = this.sary; let indexes; p.b(function() { indexes = sary.search(word); }, 'search/' + word); /* * title * comment * url */ var str = this.sary.string; let tmp = []; let res = []; for (let i = 0, len = indexes.length; i < len; i++) { let sIndex = str.lastIndexOf("\0", indexes[i]); if (tmp.indexOf(sIndex) == -1) { tmp.push(sIndex); let eIndex = str.indexOf("\0", indexes[i]); if (sIndex != -1 && eIndex != -1) { res.push(new HatenaBookmark.Data(str.substring(sIndex, eIndex-1))); } } } return res; }, } HatenaBookmark.UserData = { get bookmarks() { this.init(); return this._bookmarks; }, get cache() HatenaBookmark.Cache, reload: function() { this._inited = false; this.cache.clear(); this.init(); }, init: function() { if (!this._inited) { let cache = HatenaBookmark.Cache.data; if (this._bookmarks) delete this._bookmarks; this._inited = true; this.createDataStructure(cache); } }, createDataStructure: function(data) { this._bookmarks = []; this.pushData(this._bookmarks, data); }, pushData: function(ary, data) { var infos = data.split("\n"); var tmp = infos.splice(0, infos.length * 3/4); var len = tmp.length; for (var i = 0; i < len; i+=3) { /* * title * comment * URL */ ary.push(new HatenaBookmark.Data(tmp[i] + "\n" + tmp[i+1] + "\n" + tmp[i+2])); } } }; let SuffixArray = function (string) { this.string = string; this.lowerString = string.toLowerCase(); this.defaultLength = 255; } SuffixArray.prototype = { make: function SuffixArray_createSuffixArray() { let string = this.lowerString; let sary = []; let saryIndex = 0; let str; let index; let dLen = this.defaultLength; p.b(function() { for (let i = 0, len = string.length; i < len; i++) { str = string.substr(i, dLen); index = str.indexOf("\n"); if (index != 0) { if (index != -1) str = str.substr(0, index); sary[saryIndex++] = [str, i]; } } }, 'create'); p.b(function() { sary.sort(function(a, b) { if (a[0] > b[0]) { return 1; } else if (a[0] < b[0]) { return -1; } return 0; }); }, 'sort'); this.sary = sary.map(function([_,i]) i); }, set sary (sary) { this._sary = sary; this._len = sary.length }, get sary () this._sary, get length () this._len, search: function SuffixArray_search(word) { let wLen = word.length; if (wLen == 0) return []; if (!this.sary) this.make(); word = word.toLowerCase(); let string = this.lowerString; let sary = this.sary; let len = this.length; let lastIndex = -1; let index = parseInt(len / 2); let floor =
/*
 * ==VimperatorPlugin==
 * @name            youtubeamp.js
 * @description     this script gives you keyboard oprations for YouTube.com.
 * @description-ja  YouTube のプレーヤーをキーボードで操作できるようにする。
 * @author          janus_wel <janus_wel@fb3.so-net.ne.jp>
 * @version         0.12
 * @minversion      2.0pre 2008/10/16
 * ==/VimperatorPlugin==
 *
 * LICENSE
 *   New BSD License
 *
 * USAGE
 *   :ytinfo
 *     プレーヤーに関しての情報を表示する。今のところバージョンだけ。
 *   :ytpause
 *     再生 / 一時停止を切り替える。
 *   :ytmute
 *     音声あり / なしを切り替える。
 *   :ytsize
 *     最大化 / ノーマルを切り替える。動いてない。
 *   :ytseek [position]
 *     指定した場所にシークする。秒数で指定が可能。
 *     指定なしの場合一番最初にシークする。
 *   :ytseek! delta
 *     現在の位置から delta 分離れた所にシークする。秒数で指定が可能。
 *     マイナスを指定すると戻る。指定なしの場合変化しない。
 *   :ytvolume [volume]
 *     ボリュームを設定する。 0 ~ 100 が指定できる。
 *     指定なしの場合 100 にセットする。
 *   :ytvolume! delta
 *     ボリュームを現在の値から変更する。 -100 ~ +100 を指定可能。
 *     指定なしの場合変化しない。
 *
 * SEE ALSO
 *   http://code.google.com/apis/youtube/js_api_reference.html
 *
 * HISTORY
 *   2008/10/07 ver. 0.10   - initial written.
 * */

(function() {

Function.prototype.bind = function(object) {
    var __method = this;
    return function() {
        return __method.apply(object, arguments);
    };
};

// class definition
// YouTubePlayerController Class
function YouTubePlayerController() {
    this.initialize.apply(this, arguments);
}
YouTubePlayerController.prototype = {
    initialize: function() {
        this.fuller = this._changeToFull.bind(this);
    },

    constants: {
        VERSION: '0.12',

        CARDINAL_NUMBER: 10,

        YOUTUBE_DOMAIN: '.youtube.jp',
        YOUTUBE_URL:    '^http://[^.]+\\.youtube\\.com/',
        WATCH_URL:      'http://[^.]+\\.youtube\\.com/watch',
        WATCH_PAGE:     1,

        PLAYER_NODE_ID: 'movie_player',

        STATE_PLAYING: 1,

        SIZE_WIDTH_DEFAULT:  480,
        SIZE_HEIGHT_DEFAULT: 385,

        NAME_PLAYER_VERSION: 'PLAYER_VERSION',

        SEEKTO_DEFAULT:   0,
        SEEKBY_DEFAULT:   0,
        VOLUMETO_DEFAULT: 100,
        VOLUMEBY_DEFAULT: 0,

        HIDE_NODES: [
            'old-masthead',
            'watch-vid-title',
            'watch-other-vids',
            'old-footer',
            'copyright',
            'watch-main-area',
            'watch-comments-stats',
            'watch-video-response',
            'chrome-promo',
            'watch-video-quality-setting',
        ],
    },

    getControllerVersion: function() { return this.constants.VERSION; },

    pagecheck: function() {
        if(this.getURL().match(this.constants.WATCH_URL)) return this.constants.WATCH_PAGE;
        throw new Error('current tab is not watch page on youtube.com');
    },

    getURL: function() { return liberator.modules.buffer.URL; },

    _player: function() {
        if(this.pagecheck() === this.constants.WATCH_PAGE) {
            let player = this._getElementById(this.constants.PLAYER_NODE_ID);
            if(! player) throw new Error('player is not found');

            return player;
        }
        return null;
    },

    togglePlay: function() {
        var p = this._player();
        (p.getPlayerState() !== this.constants.STATE_PLAYING)
            ? p.playVideo()
            : p.pauseVideo();
    },

    toggleMute: function() {
        var p = this._player();
        p.isMuted() ? p.unMute() : p.mute();
    },

    toggleSize: function() {
        var p = this._player();
        (p.width == this.constants.SIZE_WIDTH_DEFAULT && p.height == this.constants.SIZE_HEIGHT_DEFAULT)
            ? this._fullSize()
            : this._normalSize();
    },

    _changeToFull: function() {
        var p = this._player();
        setTimeout(function() {
            p.width = content.innerWidth;
            p.height = content.innerHeight;
        }, 0);
    },

    _getElementById: function(id) {
        var e = window.content.document.getElementById(id);
        if(!e) return null;

        return e.wrappedJSObject
            ? e.wrappedJSObject
            : e;
    },

    _fullSize: function() {
        var b = this._getElementById('baseDiv');
        this.defMargin = b.style.margin;
        this.defPadding = b.style.padding;
        this.defWidth = b.style.width;
        b.style.margin = 0;
        b.style.padding = 0;
        b.style.width = '100%';

        for(let i=0, max=this.constants.HIDE_NODES.length ; i<max ; ++i) {
            let h = this._getElementById(this.constants.HIDE_NODES[i]);
            if(h) { h.style.display = 'none'; }
        }

        this._changeToFull();

        window.addEventListener(
            'resize',
            this.fuller,
            false
        );
    },

    _normalSize: function() {
        var b = this._getElementById('baseDiv');
        b.style.margin  = this.defMargin;
        b.style.padding = this.defPadding;
        b.style.width   = this.defWidth;

        for(let i=0, max=this.constants.HIDE_NODES.length ; i<max ; ++i) {
            let h = this._getElementById(this.constants.HIDE_NODES[i]);
            if(h) { h.style.display = 'block'; }
        }

        var p = this._player();
        p.width = this.constants.SIZE_WIDTH_DEFAULT;
        p.height = this.constants.SIZE_HEIGHT_DEFAULT;

        window.removeEventListener(
            'resize',
            this.fuller,
            false
        );
    },

    seekTo: function(position) {
        if(position) {
            if(position.match(/^(\d+):(\d+)$/)) {
                position = parseInt(RegExp.$1, this.constants.CARDINAL_NUMBER) * 60
                    + parseInt(RegExp.$2, this.constants.CARDINAL_NUMBER);
            }
            if(isNaN(position)) throw new Error('assign unsigned number : seekTo()');
        }
        else position = this.constants.SEEKTO_DEFAULT;

        var p = this._player();
        p.seekTo(position);
    },

    seekBy: function(delta) {
        if(delta) {
            if(isNaN(delta)) throw new Error('assign signed number : seekBy()');
        }
        else delta = this.constants.SEEKBY_DEFAULT;

        var p = this._player();
        var position = p.getCurrentTime();
        position += parseInt(delta, this.constants.CARDINAL_NUMBER);

        p.seekTo(position);
    },

    volumeTo: function(volume) {
        if(volume) {
            if(isNaN(volume)) throw new Error('assign unsigned number : volumeTo()');
        }
        else volume = this.constants.VOLUMETO_DEFAULT;

        var p = this._player();
        p.setVolume(volume);
    },

    volumeBy: function(delta) {
        if(delta) {
            if(isNaN(delta)) throw new Error('assign signed number : volumeBy()');
        }
        else delta = this.constants.VOLUMEBY_DEFAULT;

        var p = this._player();
        var volume = p.getVolume();
        volume += parseInt(delta, this.constants.CARDINAL_NUMBER);

        p.setVolume(volume);
    },
};

// global object
var controller = new YouTubePlayerController();

// command register
liberator.modules.commands.addUserCommand(
    ['ytinfo'],
    'display player information',
    function() {
        try {
            let info = [
                'controller version : ' + controller.getControllerVersion(),
            ].join('\n');
            liberator.echo(info, liberator.modules.commandline.FORCE_MULTILINE);
        }
        catch(e) { liberator.echoerr(e); }
    },
    {}
);

liberator.modules.commands.addUserCommand(
    ['ytpause'],
    'toggle play / pause',
    function() {
        try      { controller.togglePlay(); }
        catch(e) { liberator.echoerr(e); }
    },
    {}
);

liberator.modules.commands.addUserCommand(
    ['ytmute'],
    'toggle mute',
    function() {
        try      { controller.toggleMute(); }
        catch(e) { liberator.echoerr(e); }
    },
    {}
);

liberator.modules.commands.addUserCommand(
    ['ytseek'],
    'controll seek bar',
    function(args) {
        try {
            let arg = (args.length > 1)
                ? args[0].toString()
                : args.string;
            args.bang ? controller.seekBy(arg) : controller.seekTo(arg);
        }
        catch(e) { liberator.echoerr(e); }
    },
    {
        bang: true,
    }
);

liberator.modules.commands.addUserCommand(
    ['ytvolume'],
    'controll volume',
    function(args) {
        try {
            let arg = (args.length > 1)
                ? args[0].toString()
                : args.string;
            args.bang ? controller.volumeBy(arg) : controller.volumeTo(arg);
        }
        catch(e) { liberator.echoerr(e); }
    },
    {
        bang: true,
    }
);

liberator.modules.commands.addUserCommand(
    ['ytsize'],
    'toggle video size',
    function() {
        try      { controller.toggleSize(); }
        catch(e) { liberator.echoerr(e); }
    },
    {}
);

})()

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