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; })(); n386' href='#n386'>386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590