var PLUGIN_INFO = xml` Auto Detect Link Find (next|previous) link, and jump. (次|前)っぽいページへのリンクを探してジャンプ 1.8.3 anekos 2.0pre https://github.com/vimpr/vimperator-plugins/raw/master/auto_detect_link.js Creative Commons Attribution-Share Alike 3.0 Unported // 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: 設定を一時的に上書きする。省略可。 ==== 返値 ==== リンクのURIなど以下のプロパティを持つオブジェクト uri: アドレス。 text: リンクテキストなど frame: リンクの存在するフレームの Window オブジェクト element: リンクの要素 === autoDetectLink.go(next, setting) === (次|前)へのリンクに移動。 引数は detect と同じ。 === example === 履歴を使用しないで、前のリンクを探す。: >|| liberator.plugins.autoDetectLink.detect(false, {useBackHistory: false}); ||< ]]> `; (function () { 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, displayDelay: 500, ignoreId: false }; //////////////////////////////////////////////////////////////// // 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; try { ap_cache = 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); } } catch (e) { liberator.log('ap_cache evaluationg error. no autopagerize?: \n' + e); } //////////////////////////////////////////////////////////////// // functions //////////////////////////////////////////////////////////////// // 空白を function removeSpace (str) str.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' '); // 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) buffer.followLink(elem, liberator.CURRENT_TAB); // 開いたURIなどの表示 function displayOpened (link) { var msg = 'open: ' + link.type + ' <' + removeSpace(link.text) + '> ' + link.uri; setTimeout(function () liberator.echo(msg, commandline.FORCE_SINGLELINE), gv().displayDelay); } // リンクを開く function open (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)); // ID っぽい文字か考えてみる! // 数字だけで長いのは ID っぽい! // 西暦っぽいのは無視しない方が良いかも。 // 根拠はないが、1980-2029 の範囲で。 // 後方00 が含まれているパターンは、インクリメントしてもいい気がする // 830000 => 830001 // XXX 根拠があやしぎる! function likeID (s) /^\d{6,}$/.test(s) && !/^(19[89]|20[012])\d/.test(s) && !/00\d\d$/.test(s); // (次|前)の数字文字列リストを取得 function succNumber (n, next, ignoreId) { if (ignoreId && likeID(n)) return []; 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, ignoreId) { 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, ignoreId).reverse().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, rel: it.rel, 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; } // 全フレームの URL を得る function getAllLocations (content) { let result = [content.location.href]; if (content.frames) { for (let i = 0, l = content.frames.length; i < l; i++) { result = result.concat(getAllLocations(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); // rel="prev|next" { let relValue = next ? /(?:^|[ \t\r\n])next(?:[ \t\n\r]|$)/ : /(?:^|[ \t\r\n])prev(?:[ \t\n\r]|$)/; let link = find(links, function (link) ((typeof link.rel == 'string') && relValue.test(link.rel.toLowerCase()))); if (link) return link; } // keywords { let link; if (patterns.some(function (pattern) { link = find(links, function (link) match(pattern, link)); return link ? true : false; })) return link; } // succ let succs = []; getAllLocations(window.content).forEach(function (uri) { succs = succs.concat(succURI(uri, next, setting.ignoreId)); }); 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.remove([modes.NORMAL], gv().nextMappings); mappings.addUserMap( [modes.NORMAL], gv().nextMappings, 'Go next', function () go(true) ); } if (gv().backMappings.length) { mappings.remove([modes.NORMAL], gv().backMappings); mappings.addUserMap( [modes.NORMAL], gv().backMappings, 'Go back', function () go(false) ); } liberator.log('auto_detect_link.js loaded'); })();