aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--auto_detect_link.js473
1 files changed, 473 insertions, 0 deletions
diff --git a/auto_detect_link.js b/auto_detect_link.js
new file mode 100644
index 0000000..698ae03
--- /dev/null
+++ b/auto_detect_link.js
@@ -0,0 +1,473 @@
+// ==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); } })();