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 = Math.floor; let ceil = Math.ceil; let str; let range = index; while (lastIndex != index) { lastIndex = index; str = string.substr(sary[index], wLen); if (word < str) { range = floor(range / 2); index = index - range; } else if (word > str) { range = ceil(range / 2); index = index + range; } else { let res = [sary[index]]; let start = index; while (string.substr(sary[--start], wLen) == word) res.unshift(sary[start]); let end = index; while (string.substr(sary[++end], wLen) == word) res.push(sary[end]); res.sort(function(a, b) a - b); return res; } } return []; } } return HatenaBookmark; })();