/*** BEGIN LICENSE BLOCK {{{ Copyright (c) 2008 suVene Released under the GPL license http://www.gnu.org/copyleft/gpl.html }}} END LICENSE BLOCK ***/ // PLUGIN_INFO//{{{ var PLUGIN_INFO = xml` nextlink mapping "[[", "]]" by AutoPagerize XPath. AutoPagerize 用の XPath より "[[", "]]" をマッピングします。 suVene hogelog 0.3.9 GPL 2.2pre 2.2pre https://github.com/vimpr/vimperator-plugins/raw/master/nextlink.js || let g:nextlink_followlink = "true" ||< と設定することにより、"[[", "]]" の動作は、カレントのタブに新しくページを読み込むようになります。 >|| let g:nextlink_prevmap = "[n" let g:nextlink_nextmap = "]n" ||< のように設定することにより、"[[", "]]" 以外のキーに割り当てることができます。 SITEINFOが無い場合の処理を >|| let g:nextlink_nositeinfo_act = "f" ||< のように設定できます。現在は f: Vimperatorの"[[", "]]"の動作 e: マッチするSITEINFOが無いことを知らせる(デフォルト設定) n: 何もしない が設定可能です /info//nextlink-local-siteinfo に >|| [ { "url": "^http://[^.]+\\.google\\.(?:[^.]+\\.)?[^./]+/search\\b", "nextLink": "id('navbar')//td[last()]/a", "pageElement": "id('res')/div", "exampleUrl": "http://www.google.com/search?q=nsIObserver", }, ] ||< のような JSON を置くことでローカルで SITEINFO を設定できます == TODO == ]]> `; //}}} liberator.plugins.nextlink = (function() { // initialize //{{{ if (!liberator.plugins.libly) { liberator.log("nextlink: needs _libly.js"); return; } var libly = liberator.plugins.libly; var $U = libly.$U; var logger = $U.getLogger("nextlink"); var $H = Cc["@mozilla.org/browser/global-history;2"].getService(Ci.nsIGlobalHistory2); const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; const UUID = "{3b72c049-a347-4777-96f6-b128fc76ed6a}"; // siteinfo cache key const DEFAULT_PREVMAP = "[["; const DEFAULT_NEXTMAP = "]]"; var prevMap = liberator.globalVariables.nextlink_prevmap || DEFAULT_PREVMAP; var nextMap = liberator.globalVariables.nextlink_nextmap || DEFAULT_NEXTMAP; var isFollowLink = typeof liberator.globalVariables.nextlink_followlink == "undefined" ? false : $U.eval(liberator.globalVariables.nextlink_followlink); const MICROFORMAT = { url: "^https?://.", nextLink: '//a[translate(normalize-space(@rel), "NEXT", "next")="next"] | //link[translate(normalize-space(@rel), "NEXT", "next")="next"]', insertBefore: '//*[contains(concat(" ", @class, " "), " autopagerize_insert_before ")]', pageElement: '//*[contains(concat(" ", @class, " "), " autopagerize_page_element ")]', } const nositeinfoActions = { // vimperator [[, ]] action f: function(doc, count) { if (count < 0) { return buffer.followDocumentRelationship("previous"); } return buffer.followDocumentRelationship("next"); }, e: function(doc, count) { var url = doc.location.href; return liberator.echo("No SITEINFO match " + url); }, n: function() true, }; var actpattern = liberator.globalVariables.nextlink_nositeinfo_act || "e"; var nositeinfoAct = nositeinfoActions[actpattern]; var localSiteinfo = storage.newMap("nextlink-local-siteinfo", {store: false}); if (localSiteinfo) localSiteinfo = [ info for ([ i, info ] in localSiteinfo) ]; var pageNaviCss = ; //}}} var NextLink = function() {//{{{ this.initialize.apply(this, arguments); }; NextLink.prototype = { initialize: function(pager) { this.initialized = false; this.siteinfo = []; this.pager = pager; this.browserModes = config.browserModes || [ modes.NORMAL, modes.VISUAL ]; this.is2_0later = config.autocommands.some(function ([ k, v ]) k == "DOMLoad"); // toriaezu var wedata = new libly.Wedata("AutoPagerize"); wedata.getItems(24 * 60 * 60 * 1000, null, $U.bind(this, function(isSuccess, data) { if (!isSuccess) return; this.siteinfo = data.map(function(item) item.data); if (localSiteinfo) this.siteinfo = this.siteinfo.concat(localSiteinfo); this.siteinfo = this.siteinfo.sort(function(a, b) b.url.length - a.url.length); // sort url.length desc this.initialized = true; }) ); this.customizeMap(this); }, initDoc: function(context, doc) { var value = doc[UUID] = {}; value.siteinfo = this.getSiteinfo(doc); this.pager.initDoc(context, doc); }, getSiteinfo: function(doc) { function valid(prop) $U.getNodesFromXPath(MICROFORMAT[prop], doc).length > 0; if (valid("nextLink") && valid("pageElement")) return MICROFORMAT; var url = doc.location.href; for (let i = 0, len = this.siteinfo.length; i < len; i++) { if (url.match(this.siteinfo[i].url) && this.siteinfo[i].url != "^https?://.") { return this.siteinfo[i]; } } return null; }, nextLink: function(count) { if (!this.initialized) { liberator.echo("before initialized."); return false; } var doc = window.content.document; if (!doc[UUID]) this.initDoc(this, doc); this.pager.nextLink(doc, count); }, customizeMap: function(context) { mappings.addUserMap(context.browserModes, [ prevMap ], "customize by nextlink.js", function(count) context.nextLink(count > 0 ? -1 * count : -1), { count: true }); mappings.addUserMap(context.browserModes, [ nextMap ], "customize by nextlink.js", function(count) context.nextLink(count > 0 ? count : 1), { count: true }); }, };//}}} var Autopager = function() {};//{{{ Autopager.prototype = { initDoc: function(context, doc) { doc[UUID].loadURLs = []; if (context.is2_0later) { let css = $U.xmlToDom(pageNaviCss, doc); let node = doc.importNode(css, true); doc.body.insertBefore(node, doc.body.firstChild); //doc.body.appendChild(css); } }, nextLink: function(doc, count) { var value = doc[UUID]; // TODO: support MICROFORMAT // rel="next", rel="prev" if (!value.siteinfo && nositeinfoAct) { return nositeinfoAct(doc, count); } var curPage = this.getCurrentPage(doc); logger.log(curPage); var page = (count < 0 ? Math.round : Math.floor)(curPage + count); if (page <= 1) { value.isLoading = false; doc.defaultView.scrollTo(0, 0); return true; } if (this.focusPagenavi(doc, page)) { value.isLoading = false; return true; } if (value.isLoading) { logger.echo("loading now..."); return false; } value.isLoading = true; if (value.terminate) { value.isLoading = false; logger.echo("terminated."); return false; } var req = this.createNextRequest(doc); if (!req) { value.isLoading = false; let win = doc.defaultView; win.scrollTo(0, win.scrollMaxY); logger.echo("end of pages."); return true; } req.addEventListener("success", $U.bind(this, this.onSuccess)); req.addEventListener("failure", $U.bind(this, this.onFailure)); req.addEventListener("exception", $U.bind(this, this.onFailure)); req.get(); }, onSuccess: function(res) { var doc = res.req.options.doc; var url = doc.location.href; var value = doc[UUID]; var pages = res.getHTMLDocument(value.siteinfo.pageElement) var resDoc = res.doc; var reqUrl = res.req.url; var [ next ] = $U.getNodesFromXPath(value.siteinfo.nextLink, resDoc); value.loadURLs.push(reqUrl); value.next = next; value.isLoading = false; // set reqUrl link-state visited $H.addURI(makeURI(reqUrl), false, true, makeURI(url)); if (!pages || pages.length == 0) return; var addPageNum = this.getPageNum(doc) + 1; this.addPage(doc, resDoc, pages, reqUrl, addPageNum); this.focusPagenavi(doc, addPageNum); }, addPage: function(doc, resDoc, pages, reqUrl, addPageNum) { var url = doc.location.href; var value = doc[UUID]; if (!value.insertPoint) value.insertPoint = this.getInsertPoint(doc, value.siteinfo); var insertPoint = value.insertPoint; this.insertRule(doc, addPageNum, reqUrl, pages[0], insertPoint); pages.forEach(function(elem) { var pe = resDoc.importNode(elem, true); insertPoint.parentNode.insertBefore(pe, insertPoint); }); return true; }, onFailure: function(res) { logger.log("onFailure"); var doc = res.req.options.doc; var url = doc.location.href; var value = doc[UUID]; value.isLoading = false; value.terminate = true; logger.echoerr("nextlink: loading failed. " + "[" + res.status + "]" + res.statusText + " > " + res.req.url); }, focusPagenavi: function(doc, page) { var xpath = '//*[@id="vimperator-nextlink-' + page + '"]'; var [ elem ] = $U.getNodesFromXPath(xpath, doc); var win = doc.defaultView; if (elem) { let p = $U.getElementPosition(elem); win.scrollTo(0, p.top); return true; } return false; }, createNextRequest: function(doc) { var value = doc[UUID]; var url = doc.location.href; var next = value.next; if (!next) [ next ] = $U.getNodesFromXPath(value.siteinfo.nextLink, doc); if (!next) return false; var reqUrl = $U.pathToURL(next, url, doc); if (value.loadURLs.some(function(url) url == reqUrl)) return false; var req = new libly.Request( reqUrl, null, { asynchronous: true, encoding: doc.characterSet, doc: doc } ); return req; }, getPageNum: function(doc) { var xpath = '//*[@class="vimperator-nextlink-page"]'; var page = 1 + $U.getNodesFromXPath(xpath, doc).length; return page; }, getCurrentPage: function(doc) { var xpath = '//*[@class="vimperator-nextlink-page"]'; var markers = $U.getNodesFromXPath(xpath, doc); var win = doc.defaultView; var curPos = win.scrollY; // top of page if (curPos <= 0) return 1.0; // bottom of page if (curPos >= win.scrollMaxY) { if (markers.length > 0) { let lastMarker = $U.getElementPosition(markers[markers.length-1]).top; if (curPos <= lastMarker) return markers.length + 1; } return markers.length + 1.5; } // return n.5 if between n and n+1 var page = 1.0; for (let i = 0, len = markers.length; i < len; i++) { let pos = $U.getElementPosition(markers[i]).top; if (curPos == pos) return page + 1; if (curPos < pos) return page + 0.5; ++page; } return page + 0.5; }, getInsertPoint: function(doc, siteinfo) { var insertPoint, lastPageElement; if (siteinfo.insertBefore) [ insertPoint ] = $U.getNodesFromXPath(siteinfo.insertBefore, doc); if (!insertPoint) { let elems = $U.getNodesFromXPath(siteinfo.pageElement, doc); if (elems.length > 0) lastPageElement = elems.pop(); } if (lastPageElement) insertPoint = lastPageElement.nextSibling || lastPageElement.parentNode.appendChild(doc.createTextNode(" ")); return insertPoint; }, insertRule: function(doc, addPageNum, reqUrl, page, insertPoint) { var p = doc.createElementNS(HTML_NAMESPACE, "p"); p.id = "vimperator-nextlink-" + addPageNum; p.innerHTML = 'page: ' + addPageNum + ""; p.className = "vimperator-nextlink-page"; var tagName; if (page && page.tagName) tagName = page.tagName.toLowerCase(); if (tagName == "tr") { let insertParent = insertPoint.parentNode; let colNodes = getElementsByXPath("child::tr[1]/child::*[self::td or self::th]", insertParent); let colums = 0; for (let i = 0, l = colNodes.length, f = function(col) { colums += parseInt(col, 10) || 1; }; i < l; f(colNodes[i++].getAttribute("colspan"))); let td = insertParent.insertBefore(doc.createElement("tr"), insertPoint) .appendChild(doc.createElement("td")); td.setAttribute("colspan", colums); td.appendChild(p); } else if (tagName == "li") { insertPoint.parentNode.insertBefore(doc.createElementNS(HTML_NAMESPACE, "li"), insertPoint) .appendChild(p); } else { insertPoint.parentNode.insertBefore(p, insertPoint); } }, }; //}}} var FollowLink = function() {};//{{{ FollowLink.prototype = { initDoc: function(context, doc) { }, nextLink: function(doc, count) { var url = doc.location.href; var value = doc[UUID]; function followXPath(xpath) { var [ elem ] = $U.getNodesFromXPath(xpath, doc); if (elem) { let tagName = elem.tagName.toLowerCase(); if (tagName == "link") { liberator.open(elem.href); } else { buffer.followLink(elem, liberator.CURRENT_TAB); } return true; } return false; } if (count < 0) { let xpath = [ "link", "a" ].map(function(e) "//" + e + '[translate(normalize-space(@rel), "PREV", "prev")="prev"]') .join(" | "); if (followXPath(xpath)) return; buffer.followDocumentRelationship("previous"); } else { let xpath = [ "link", "a" ].map(function(e) "//" + e + '[translate(normalize-space(@rel), "NEXT", "next")="next"]') .join(" | "); if (followXPath(xpath)) return; if (value.siteinfo && followXPath(value.siteinfo.nextLink)) return; buffer.followDocumentRelationship("next"); } } }; //}}} var instance = new NextLink((isFollowLink ? new FollowLink() : new Autopager())); return instance; })(); // vim: set fdm=marker sw=2 ts=2 sts=0 et: // hmm, the code below generates a log: Invalid argument for followLink. buffer.followLink(elem, liberator.CURRENT_TAB); }, /// Gives focus to the main Canvas, to make all keys working well. focusMainFrame:function () { GViMail.getMainCanvas().contentWindow.focus(); }, /// On TabSelect (if Gmail Tab), we will give focus to the main canvas. get isGmail () (/^https?:\/\/mail\.google\.com\//.test(buffer.URL)), /// when you type some key to make an action, habitually, the main canvas looses focus. /// we will add an EventListener on keypress to avoid this. preventLooseFocus:function() { if (liberator.mode == modes.NORMAL && !liberator.mode.isRecording && !(modes.extended & modes.MENU) && !modes.passNextKey && !modes.passAllKeys) { GViMail.focusMainFrame(); } } }; mappings.addUserMap(GViMail.modes, ["zM"], "Closes all fold", function () { GViMail.clickImage('Dm2exe', 'collapse_icon'); }); mappings.addUserMap(GViMail.modes, ["zR"], "Opens all fold", function () { GViMail.clickImage('kPoXId', 'expand_icon'); }); // Let's build the Gmail(v2)-custom hinttags. Follow the comments to understand. var gmail_v2_hinttags = "//span[@selector]" // Menu Settings, Older version, Compose Mail, Inbox, Starred .. Contacts, Labels, turn on/off chat // The [not(ancestor::tr//td[@class='mka4te'])] is to avoid interferring with //td[@class='mka4te']/ancestor::tr/td[5], see Select message in the list below + " | //span[@role='link'][not(ancestor::tr//td[@class='mka4te'])]" // Refresh, Back to "label", Reply to all, Forward, Filter messages like this, ... // You could just use //div[@act] here, but there appears 4 unwanted hints when first-viewing a message + " | //div[@act][not(ancestor::div[contains(concat(' ', @class, ' '), ' zWKgkf ')]) or (ancestor::div[contains(concat(' ', @class, ' '), ' zWKgkf ') and contains(@style, 'visibility')])]" // More actions, Toolbar buttons on RTE (don't use RTE, plain ascii mails are sexier) + " | //*[@unselectable='on']" // Fold and UnFold messages in thread that has an excerpt displayed in grey + " | //div[contains(concat(' ', @class, ' '), ' IUCKJe ')]" // UnFold messages in thread when no excerpt is displayed (blank line) // Such <div>s have a class XoqCub, have another <div> child having the class YrHFdf, and there is no table il all their descendants + " | //*[contains(concat(' ', @class, ' '), ' XoqCub ')]/div[@class='YrHFdf'][count(descendant-or-self::table)=0]" // Star on message list + " | //td[@class='mka4te']/img" // Star on thread list (same subject) + " | //td/span[starts-with(@class, 'lHQn1d')]/img" // Delete all spam messages now + " | //*[@class='rj1J6b'" // Invite x@y.z to Gmail. + " or @class='YCDlS'" // When you delete any message in a thread view, there are links saying "n deleted messages in this conversation. View message or delete forever." + " or @class='u1T3K' or @class='iVE0ue'" // Hide filter options (settings) + " or @class='u7uAnb']" // Select message in the list + " | //td[@class='mka4te']/ancestor::tr/td[5]" // Change picture [Settings] ==> next step still not working + " | //div[@class='c3pyI']/span" // Attach a file + Add event invitation + Rich formatting|Plain text + " | //*[contains(concat(' ', @class, ' '), ' MRoIub ')]" // Check spelling + " | //span[@class='mrKIf']" // Everything that is displayed as image (+ Edit labels) + " | //img[contains(@src, 'cleardot.gif')]" // Reply + Reply to all + Forward + show/hide details + Edit labels // We will not select divs that contains any hintable elements inside + " | //*[@idlink][count(descendant-or-self::span[@role='link'])=0 and count(descendant-or-self::a)=0]" // <label>|x + " | //table[@class='Ir5Jyf']//span" // Settings> Accounts> make_default|edit_info|delete|View_history|Check_mail_now + " | //*[contains(concat(' ', @class, ' '), ' GaVz0 ')]" // Update conversation, Ignore (when someone just posted a message on the thread you're reading & editing) + " | //*[contains(concat(' ', @class, ' '), ' Gf76kb ')]" + " | //*[contains(concat(' ', @class, ' '), ' GRpVjf ')]" //Recently changed to this ... // Show|Hide quoted text + " | //span[contains(concat(' ', @class, ' '), ' WQ9l9c ')]" // + " | //div[contains(concat(' ', @class), ' goog-menuitem')]"; // We provide limited support for Gmail(v1) var gmail_v1_hinttags = "//*[contains(@class, 'lk ') or @class='msc' or @class='ll' or @class='setl' or @class='lkw' or starts-with(@class, 'sc ')] | //tr[@class='rr' or @class='ur']/td[position()=5] | //div/span[contains(@class, 'bz_rbbb')] | //span[@class='l' and contains(@id, 'sl_')]" ; var gmail_hints = use_gmail_v1 ? gmail_v1_hinttags : ""; if (use_gmail_v2) gmail_hints = gmail_hints + (gmail_hints ? " | " : "") + gmail_v2_hinttags; gmail_hints = gmail_hints + (gmail_hints ? " | " : "") + options['hinttags']; // Now: override default hinttags. Override is not the true wording, I'd rather say extend. /*options.add(["hinttags", "ht"], "XPath string of hintable elements activated by 'f' and 'F'", // Gmail uses span[@selector] for labels in the line Select: All, None, Read, Unread, Starred, Unstarred "string", gmail_hints);*/ // This is not the most elegant solution to do this, but I don't manage to find the correct one myself... options.get("hinttags").value=gmail_hints; // When navigation keys (and others) no longer work, type zf to focus to the main frame mappings.addUserMap(GViMail.modes, ["zf"], "Focus main frame", function () { GViMail.focusMainFrame(); }); getBrowser().mTabBox.addEventListener('TabSelect', function(event){ if (GViMail.isGmail) window.setTimeout(function(){GViMail.focusMainFrame();}, 100); }, false); window.addEventListener('keypress', function () { if (GViMail.isGmail) GViMail.preventLooseFocus(); }, true); if (use_gvimail_css && (typeof liberator.globalVariables.styles == 'undefined' || liberator.globalVariables.styles == '')) { liberator.globalVariables.styles = 'style,gvimail'; } })(); // vim:noet: