// ==VimperatorPlugin== // @name Auto Detect Link // @description-ja (次|前)っぽいページへのリンクを探してジャンプ // @license Creative Commons 2.1 (Attribution + Share Alike) // @version 1.3 // @author anekos (anekos@snca.net) // ==/VimperatorPlugin== // // Usage: // デフォルトの設定では、"]]" "[[" を上書きします。 // ]] 次っぽいページへ // [[ 前っぽいページへ // // Setting: // liberator.globalVariables.autoDetectLink // nextPatterns: // backPatterns: // (次|前)のパターンの配列。 // 要素は、 // ・リンク文字列に対する(正規表現|文字列) // ・リンクに対する関数のリスト // nextMappings: // backPatterns: // (次|前)移動のマッピング(Array) // useNextHistory: // useBackHistory: // 履歴を併用。 // 履歴がある場合はそっちを優先します。 // useSuccPattern: // doc_01.html のときは、 doc_02.html を次と見なす…ようなパターン。 // ファイル名に当たる部分の、数字列あるいは一文字のアルファベットが対象です。 // (つながっているアルファベットは無視されます。) // doc_02.html => doc_03.html // doc_a.html => doc_b.html // force: // (次|前)っぽいURIを捏造してそこに移動します。 // useAutoPagerize: // AutoPagerize のキャッシュを利用します。 // (ただし、"次" へのリンクにしか使われません) // // example: // :js liberator.globalVariables.autoDetectLink = {nextPatterns: [/next/, /次/]} // // Function: // (次|前)へのリンクを検出する。 // liberator.plugins.autoDetectLink.detect(next, setting) // next: 次のリンクを探すときは、true。 // setting: 設定を一時的に上書きする。省略可。 // return: リンクのURIなどを含んだオブジェクト // uri: アドレス。 // text: リンクテキストなど // frame: リンクの存在するフレームの Window オブジェクト // element: リンクの要素 // // (次|前)へのリンクに移動。 // liberator.plugins.autoDetectLink.go(next, setting) // 引数は detect と同じ。 // // example: // 履歴を使用しないで、前のリンクを探す。 // liberator.plugins.autoDetectLink.detect(false, {useBackHistory: false}); // // Note: // 単純なリンクと、フォームのボタンを検出できます。 // // License: // http://creativecommons.org/licenses/by-sa/2.1/jp/ // http://creativecommons.org/licenses/by-sa/2.1/jp/deed // // TODO: // input / form // history (function () { try { liberator.log('auto_detect_link.js loading'); //////////////////////////////////////////////////////////////// // default setting //////////////////////////////////////////////////////////////// let defaultSetting = { nextPatterns: [ //[NnNn][EeEe][XxXx][TtTt]/, /[Nn\uff2e\uff4e][Ee\uff25\uff45][Xx\uff38\uff58][Tt\uff34\uff54]/, //[FfFf](?:[OoOo][RrRr])?[WwWw](?:[AaAa][RrRr])?[DdDd]/, /[Ff\uff26\uff46](?:[Oo\uff2f\uff4f][Rr\uff32\uff52])?[Ww\uff37\uff57](?:[Aa\uff21\uff41][Rr\uff32\uff52])?[Dd\uff24\uff44]/, //^\s*(?:次|つぎ)[への]/, /つづく|続/, /次|つぎ/, /進む/, /^\s*(?:\u6b21|\u3064\u304e)[\u3078\u306e]/, /\u3064\u3065\u304f|\u7d9a/, /\u6b21|\u3064\u304e/, /\u9032\u3080/, //^\s*>\s*$/, />+|≫/ /^\s*>\s*$/, />+|\u226b/ ], backPatterns: [ //[BbBb][AaAa][CcCc][KkKk]/, /[PpPp][RrRr][EeEe][VvVv]/, /[Bb\uff22\uff42][Aa\uff21\uff41][Cc\uff23\uff43][Kk\uff2b\uff4b]/, /[Pp\uff30\uff50][Rr\uff32\uff52][Ee\uff25\uff45][Vv\uff36\uff56]/, //^\s*前[への]/, /前/, /戻る/, /^\s*\u524d[\u3078\u306e]/, /\u524d/, /\u623b\u308b/, //^\s*<\s*$/, /<+|≪/ /^\s*<\s*$/, /<+|\u226a/ ], nextMappings: [']]'], backMappings: ['[['], useSuccPattern: true, useNextHistory: false, useBackHistory: false, //clickButton: true, force: false, useAutoPagerize: true, }; //////////////////////////////////////////////////////////////// // setting //////////////////////////////////////////////////////////////// let _gv; // 評価を遅延するために関数にしておく function gv () { if (_gv) return _gv; if (liberator.globalVariables) { if (!liberator.globalVariables.autoDetectLink) liberator.globalVariables.autoDetectLink = {}; _gv = liberator.globalVariables.autoDetectLink; } for (let key in defaultSetting) { if (_gv[key] == undefined) _gv[key] = defaultSetting[key]; } return _gv; } const APPREF = 'greasemonkey.scriptvals.http://swdyh.yu.to//AutoPagerize.cacheInfo'; let ap_cache = window.eval(Application.prefs.getValue(APPREF, null)); for each (let cache in ap_cache) { cache.info = cache.info.filter(function (i) 'url' in i); cache.info.sort(function (a, b) b.url.length - a.url.length); } //////////////////////////////////////////////////////////////// // functions //////////////////////////////////////////////////////////////// // Array#find function find (ary, f) { var func = (typeof f == 'function') ? f : function (v) v == f; for (let i = 0, l = ary.length; i < l; i++) { if (func(ary[i])) { return ary[i]; } } return null; } // 要素をクリックする function clickElement (elem) { liberator.log('click: ' + elem); var e = content.document.createEvent('MouseEvents'); e.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); elem.dispatchEvent(e); } // 開いたURIなどの表示 function displayOpened (link) { var msg = 'open: ' + link.type + ' <' + link.text + '> ' + link.uri; setTimeout(function () liberator.echo(msg, commandline.FORCE_SINGLELINE), 1000); liberator.log(msg); } // リンクを開く function open (link) { liberator.log(link); if (link.element) { clickElement(link.element); } else if (link.uri) { link.frame.location.href = link.uri; } displayOpened(link); } // 元の文字列、詰め込む文字、長さ function padChar (s, c, n) s.replace(new RegExp('^(.{0,'+(n-1)+'})$'), function (s) padChar(c+s, c, n)); // (次|前)の数字文字列リストを取得 function succNumber (n, next) { var m = (parseInt(n || 0, 10) + (next ? 1 : -1)).toString(); var result = [m]; if (m.length < n.length) result.unshift(padChar(m.toString(), '0', n.length)); return result; } // (次|前)の文字列リストを取得 function succString (s, next) { var result = [], d = next ? 1 : -1; var c = String.fromCharCode(s.charCodeAt(0) + d); if (('a' <= c && c <= 'z') || 'A' <= c && c <= 'Z') result.push(c); return result; } // (次|前)のURIリストを取得 function succURI (uri, next) { var urim = uri.match(/^(.+\/)([^\/]+)$/); if (!urim) return []; var [_, dir, file] = urim, result = []; // succ number let (dm, file = file, left = '', temp = []) { while (file && (dm = file.match(/\d+/))) { let [rcontext, lcontext, lmatch] = [RegExp.rightContext, RegExp.leftContext, RegExp.lastMatch]; left += lcontext; succNumber(lmatch, next).forEach(function (succ) { temp.push(dir + left + succ + rcontext); }); left += lmatch; file = rcontext; } result = result.concat(temp.reverse()); } // succ string let (dm, file = file, left = '', temp = []) { while (file && (dm = file.match(/(^|[^a-zA-Z])([a-zA-Z])([^a-zA-Z]|$)/))) { let [rcontext, lcontext] = [RegExp.rightContext, RegExp.leftContext]; left += lcontext + dm[1]; succString(dm[2], next).forEach(function (succ) { temp.push(dir + left + succ + dm[3] + rcontext); }); left += dm[1]; file = dm[3] + rcontext; } result = result.concat(temp.reverse()); } return result; } // パターンマッチング function match (pattern, link) pattern instanceof Function ? pattern(link) : !link.text ? null : pattern instanceof RegExp ? pattern.test(link.text) : link.text.toLowerCase().indexOf(pattern.toString().toLowerCase()) >= 0; // 要素が表示されているか? function isVisible (element) { var st; try { st = content.document.defaultView.getComputedStyle(element, null); return !(st.display && st.display.indexOf('none') >= 0) && (!element.parentNode || isVisible(element.parentNode)) } catch (e) { return true; } } // リンクのフィルタ function linkElementFilter (elem) isVisible(elem) && elem.href && elem.href.indexOf('@') < 0 && /^(?:(?:https?|f(?:ile|tp)):\/\/|javascript:)/.test(elem.href) && elem.textContent; // 全てのリンクを取得 // 再帰的にフレーム内のも取得する function getAllLinks (content) { var result = []; // Anchor var elements = content.document.links; for (let i = 0, l = elements.length; i < l; i++) { let it = elements[i]; if (linkElementFilter(it)) result.push({ type: 'link', frame: content, uri: it.href, text: it.textContent, element: it }); } // Form elements = content.document.getElementsByTagName('input'); for (let i = 0, l = elements.length; i < l; i++) { (function (input) { result.push({ type: 'input', frame: content, uri: input.form && input.form.action, text: input.value, click: input.click, element: input, }); })(elements[i]); } // Frame if (content.frames) { for (let i = 0, l = content.frames.length; i < l; i++) { result = result.concat(getAllLinks(content.frames[i])); } } return result; } // 上書きした設定を返す。 function getCurrentSetting (setting) { if (!setting) setting = {}; for (let n in gv()) { if (setting[n] == undefined) setting[n] = gv()[n]; } return setting; } // 相対アドレスから絶対アドレスに変換するんじゃないの? function toAbsPath (path) { with (content.document.createElement('a')) return (href = path) && href; } // AutoPagerize のデータからマッチする物を取得 function getAutopagerizeNext () { if (!ap_cache) return; var info = (function () { var uri = buffer.URL; for each (let cache in ap_cache) { for (let i = 0, l = cache.info.length; i < l; i++) { let info = cache.info[i]; if (uri.match(info.url)) return info; } } })(); if (!info) return; var doc = content.document; var result = doc.evaluate(info.nextLink, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); if (result.singleNodeValue) return result.singleNodeValue; } //////////////////////////////////////////////////////////////// // main //////////////////////////////////////////////////////////////// // リンクを探す function detect (next, setting) { try { setting = getCurrentSetting(setting); // TODO if (setting.useAutoPagerize && next) { let apnext = getAutopagerizeNext(); if (apnext) { return { type: 'aplink', frame: content, uri: apnext.href || apnext.action || apnext.value, text: apnext.textContent || apnext.title || apnext, element: apnext }; } } patterns = next ? setting.nextPatterns : setting.backPatterns; let uri = window.content.location.href; let links = getAllLinks(window.content); // keywords if (1) { let link; if (patterns.some(function (pattern) { link = find(links, function (link) match(pattern, link)); return link ? true : false; })) return link; } // succ let succs = succURI(uri, next); if (setting.useSuccPattern) { let link; if (succs.some(function (succ) { link = find(links, function (link) link.uri && (link.uri.indexOf(succ) >= 0)); return link ? true : false; })) return link; } // force if (setting.force && succs.length) return { type: 'force', uri: succs[0], text: '-force-', frame: window.content, }; } catch (e) { liberator.log(e); liberator.echoerr(e); } } // 猫又 function go (next, setting) { setting = getCurrentSetting(setting); if ((next && setting.useNextHistory) || (!next && setting.useBackHistory)) { next ? BrowserForward() : BrowserBack(); displayOpened({uri: 'history', text: next ? 'next' : 'back'}); return; } var link = detect(next, setting); if (link) open(link); } // 外部から使用可能にする。 if (liberator.plugins) liberator.plugins.autoDetectLink = {detect: detect, go: go}; //////////////////////////////////////////////////////////////// // Mappings //////////////////////////////////////////////////////////////// if (gv().nextMappings.length) { mappings.addUserMap( [modes.NORMAL], gv().nextMappings, 'Go next', function () go(true) ); } if (gv().backMappings.length) { mappings.addUserMap( [modes.NORMAL], gv().backMappings, 'Go back', function () go(false) ); } liberator.log('auto_detect_link.js loaded'); } catch (e) { alert(e); } })();