├── operational ├── package.json ├── src ├── icons │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-300.png │ ├── icon-32.png │ ├── icon-48.png │ ├── icon-512.png │ └── extensionGeneric.svg ├── manifest-chrome.json ├── manifest-firefox.json ├── _locales │ ├── zh_CN │ │ └── messages.json │ └── en_US │ │ └── messages.json ├── options.html ├── storage.js ├── usage.html ├── options.js └── nextpage.js ├── .gitignore ├── .eslintrc.js ├── privacy-policy.rst ├── Makefile ├── CHANGELOG.md ├── docs ├── intro.zh-CN.txt └── intro.en-US.txt ├── misc ├── test-functions.js └── test-regexp.js └── README /operational: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "eslint": "^4.9.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sylecn/nextpage-we/HEAD/src/icons/icon-128.png -------------------------------------------------------------------------------- /src/icons/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sylecn/nextpage-we/HEAD/src/icons/icon-16.png -------------------------------------------------------------------------------- /src/icons/icon-300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sylecn/nextpage-we/HEAD/src/icons/icon-300.png -------------------------------------------------------------------------------- /src/icons/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sylecn/nextpage-we/HEAD/src/icons/icon-32.png -------------------------------------------------------------------------------- /src/icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sylecn/nextpage-we/HEAD/src/icons/icon-48.png -------------------------------------------------------------------------------- /src/icons/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sylecn/nextpage-we/HEAD/src/icons/icon-512.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/manifest.json 2 | *.zip 3 | other-assets/ 4 | node_modules/ 5 | build/ 6 | # I know what these does, but I still don't want to commit these two files. 7 | package-lock.json 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "webextensions": true, 5 | "es6": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "rules": { 9 | "linebreak-style": [ 10 | "error", 11 | "unix" 12 | ], 13 | "semi": [ 14 | "error", 15 | "always" 16 | ], 17 | "no-unused-vars": [ 18 | "error", 19 | {"argsIgnorePattern": "^_"} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /privacy-policy.rst: -------------------------------------------------------------------------------- 1 | Privacy Policy For nextpage Add-on 2 | ================================================= 3 | 4 | last update: 2021-11-07 5 | 6 | 7 | - nextpage add-on does not collect any user data. 8 | - If you have customized user preferences, and have loged in to 9 | Firefox/Chrome/Edge, your user preferences will be synchronized across your 10 | browsers. nextpage add-on does not store your user preferences. 11 | 12 | 13 | Privacy policy config links 14 | 15 | - Firefox https://addons.mozilla.org/en-US/developers/addon/nextpage/ownership 16 | -------------------------------------------------------------------------------- /src/manifest-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_addonName__", 4 | "version": "3.0.2", 5 | "description": "__MSG_addonDescription__", 6 | "default_locale": "en_US", 7 | "icons": { 8 | "16": "icons/icon-16.png", 9 | "32": "icons/icon-32.png", 10 | "48": "icons/icon-48.png", 11 | "128": "icons/icon-128.png" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": ["*://*/*"], 16 | "js": ["storage.js", "nextpage.js"] 17 | } 18 | ], 19 | "options_page": "options.html", 20 | "permissions": [ 21 | "storage" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_addonName__", 4 | "version": "3.0.2", 5 | "description": "__MSG_addonDescription__", 6 | "default_locale": "en_US", 7 | "icons": { 8 | "16": "icons/icon-16.png", 9 | "32": "icons/icon-32.png", 10 | "48": "icons/icon-48.png", 11 | "128": "icons/icon-128.png" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": ["*://*/*"], 16 | "js": ["storage.js", "nextpage.js"] 17 | } 18 | ], 19 | "options_ui": { 20 | "page": "options.html", 21 | "browser_style": false 22 | }, 23 | "browser_specific_settings": { 24 | "gecko": { 25 | "id": "nextpage@yuanle.song" 26 | } 27 | }, 28 | "permissions": [ 29 | "storage" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: firefox chrome 2 | firefox: check 3 | @echo "building for firefox..." 4 | cd src && ln -f manifest-firefox.json manifest.json && zip -r -x manifest-chrome.json -FS ../nextpage-firefox.zip * 5 | mkdir -p build/firefox 6 | unzip -d build/firefox -q -o nextpage-firefox.zip 7 | chrome: check 8 | @echo "building for chrome..." 9 | cd src && ln -f manifest-chrome.json manifest.json && zip -r -x manifest-firefox.json -FS ../nextpage-chrome.zip * 10 | mkdir -p build/chrome 11 | unzip -d build/chrome -q -o nextpage-chrome.zip 12 | check: 13 | @echo "running eslint..." 14 | @./node_modules/.bin/eslint src/*.js # see also ./.eslintrc.js 15 | @echo "running misc/test-regexp.js..." 16 | @if which node >/dev/null 2>/dev/null; then node misc/test-regexp.js; node misc/test-functions.js; else jjs misc/test-regexp.js; jjs misc/test-functions.js; fi 17 | .PHONY: all firefox chrome check 18 | -------------------------------------------------------------------------------- /src/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addonName": { 3 | "message": "下一页", 4 | "description": "add-on name" 5 | }, 6 | "addonDescription": { 7 | "message": "在页尾处,空格键自动翻到下一页", 8 | "description": "add-on description" 9 | }, 10 | "nextPageAddOnOptions": { 11 | "message": "下一页扩展配置", 12 | "description": "add-on options title" 13 | }, 14 | "builtInConfig": { 15 | "message": "内置配置:", 16 | "description": "built-in config label" 17 | }, 18 | "userConfig": { 19 | "message": "用户配置:", 20 | "description": "user config label" 21 | }, 22 | "save": { 23 | "message": "保存", 24 | "description": "save button label" 25 | }, 26 | "help": { 27 | "message": "帮助", 28 | "description": "help button label" 29 | }, 30 | "reportBugIntro": { 31 | "message": "如果您发现软件错误并希望该错误被修复, 请", 32 | "description": "report a bug intro" 33 | }, 34 | "reportBug": { 35 | "message": "提交错误报告", 36 | "description": "report a bug" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | nextpage browser extension changelog 2 | ------------------------------------- 3 | 4 | * 2025-11-14 nextpage v3.0.2 5 | v3.x breaking change 6 | - change default binding for p key to previous-page, was history-back. 7 | 8 | bugfix 9 | - do not activate when running on onekvm/pikvm KVM web UI 10 | - better support for German locale and other locales 11 | 12 | * 2024-08-24 nextpage v2.17.4 13 | - v2.17.4 add new command: copy-download-link 14 | used to copy pt site main download link to clipboard. 15 | 16 | * 2024-07-28 nextpage v2.16.4 17 | - v2.16.4 add support for Ant Design pagination websites 18 | 19 | * 2023-12-22 nextpage v2.16.3 20 | - v2.16.2 fix issue #59: support wordpress "older posts" link 21 | - v2.16.3 fix issue #60 support "More results" link on google search 22 | 23 | * 2023-02-24 nextpage v2.13.0 24 | - v2.13.0 support shadow DOM when detecting userIsTyping() 25 | 26 | * 2023-02-14 nextpage v2.12.0 27 | - v2.12.0 added prerender/prefetch support. 28 | -------------------------------------------------------------------------------- /src/_locales/en_US/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "addonName": { 3 | "message": "nextpage", 4 | "description": "add-on name" 5 | }, 6 | "addonDescription": { 7 | "message": "use SPC key to goto next page when at the bottom of a page.", 8 | "description": "add-on description" 9 | }, 10 | "nextPageAddOnOptions": { 11 | "message": "Nextpage Add-on Options", 12 | "description": "add-on options title" 13 | }, 14 | "builtInConfig": { 15 | "message": "Built-in Config:", 16 | "description": "built-in config label" 17 | }, 18 | "userConfig": { 19 | "message": "User Config:", 20 | "description": "user config label" 21 | }, 22 | "save": { 23 | "message": "Save", 24 | "description": "save button label" 25 | }, 26 | "help": { 27 | "message": "Help", 28 | "description": "help button label" 29 | }, 30 | "reportBugIntro": { 31 | "message": "If you found a problem and would like to get it fixed, please", 32 | "description": "report a bug intro" 33 | }, 34 | "reportBug": { 35 | "message": "Report a bug", 36 | "description": "report a bug" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/intro.zh-CN.txt: -------------------------------------------------------------------------------- 1 | 在浏览在线文档或者看在线小说的时候,我习惯按空格键翻页,但是一页读完后,空格键不能自动跳到下一页。这个扩展项就是给空格键重新绑定一个功能:除了翻页,在页尾时还会自动跳转到下一页。从v2.12.0起,nextpage会预加载下一页链接以提高页面加载速度。 2 | 3 | nextpage 是自由软件,授权协议是GPLv3。源代码在 github。 4 | 5 | 错误报告 6 | =================== 7 | 如果你发现问题并且希望问题得到解决,请在这里填写错误报告: 8 | https://github.com/sylecn/ff-nextpage/issues (英文界面) 9 | 10 | nextpage 非常重视用户体验,不会干扰正常的网页浏览,如果你发现它与你的博客,邮箱,微博,用户注册表单等不兼容或者导致一些web应用功能不正常,请提交错误报告。另一方面,如果有些您经常使用的网页 nextpage 没有正常跳转到下一页,也请提交错误报告。 11 | 12 | 目前支持中文,英文和德文网页。 13 | 14 | nextpage允许您为翻页功能指定快捷键。这是默认的快捷键: 15 | 20 | 这是内部绑定这些快捷键相应的代码: 21 | 22 | (bind "SPC" 'nextpage-maybe) 23 | (bind "n" 'nextpage) 24 | (bind "p" 'history-back) 25 | 26 | 27 | 你可以关闭或修改默认的快捷键,也可以自己指定新的快捷键。具体使用方法请点击选项页的帮助按钮查看内置文档。 28 | 29 | 例如,你可以将p键绑定为上一页: 30 | 31 | (bind "p" 'previous-page) 32 | 33 | 你可以将c键绑定为复制页面标题和URL: 34 | 35 | (bind "c" 'copy-title-and-url-maybe) 36 | 37 | 本软件发布大小仅为47KB。 38 | -------------------------------------------------------------------------------- /src/icons/extensionGeneric.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nextpage options 8 | 23 | 24 | 25 | 26 |
27 |

Nextpage Add-on Options

28 |
Built-in Config:
29 | 30 |
User Config:
31 | 32 | 33 |
34 | 35 | 36 |
37 |
If you found a problem and would like to get it fixed, please report a bug.
38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /misc/test-functions.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let msgs = []; 3 | 4 | let log = function (msg) { 5 | msgs.push(msg); 6 | if (typeof console !== 'undefined') { 7 | console.log(msg); 8 | } else if (typeof print !== 'undefined') { 9 | print(msg); 10 | } 11 | }; 12 | 13 | /** 14 | * join potential relative URL to absolute URL, using given baseURL. 15 | */ 16 | let joinURL = function (baseURL, hrefValue) { 17 | const url = new URL(hrefValue, baseURL); 18 | return url.toString(); 19 | }; 20 | 21 | let testJoinURL = function () { 22 | if (! joinURL("https://example.com/abc.html", "/def.html") === "https://example.com/def.html") { 23 | log("joinURL abs path fail"); 24 | } 25 | if (! joinURL("https://example.com/p1/abc.html", "/def.html") === "https://example.com/def.html") { 26 | log("joinURL abs path fail"); 27 | } 28 | if (! joinURL("https://example.com/p1/abc.html", "def.html") === "https://example.com/p1/def.html") { 29 | log("joinURL rel path fail"); 30 | } 31 | if (! joinURL("https://example.com/p1/abc.html", "p2/def.html") === "https://example.com/p1/p2/def.html") { 32 | log("joinURL rel path fail"); 33 | } 34 | if (! joinURL("https://example.com/p1/abc.html", "https://example.com/p2/def.html") === "https://example.com/p2/def.html") { 35 | log("joinURL full url fail"); 36 | } 37 | if (! joinURL("https://example.com/p1/abc.html", "https://example.com/def.html") === "https://example.com/def.html") { 38 | log("joinURL full url fail"); 39 | } 40 | }; 41 | 42 | testJoinURL(); 43 | 44 | if (msgs.length === 0) { 45 | log("all pass."); 46 | } else { 47 | if (typeof exit !== 'undefined') { 48 | exit(1); 49 | } else if (typeof process !== 'undefined') { 50 | process.exit(1); 51 | } 52 | } 53 | }()); 54 | -------------------------------------------------------------------------------- /docs/intro.en-US.txt: -------------------------------------------------------------------------------- 1 | When reading online documents or novels, I usually use SPC to scroll page. But SPC doesn't go to next page automatically. This add-on rebinds SPC key so that it scrolls page when there is more on the page, it goes to next page when you are at the bottom of a page. You can also press n key anytime to go to next page directly. From v2.12.0, it prerender or prefetch next page link to speed up page load. 2 | 3 | nextpage is free software released under GPLv3. The source code is hosted at github. 4 | 5 | BUG Report 6 | =================== 7 | If you found a problem and would like to get it fixed, please report it here: 8 | https://github.com/sylecn/ff-nextpage/issues 9 | 10 | nextpage tries hard to not get in your way. If you find nextpage breaks your blog admin panel, some online registration form, or any kind of cool web application, please report a bug. You can also use (ignore-on "URL_REGEXP") in user config to disable this add-on on given website. 11 | 12 | Currently English, Chinese and German web pages are supported. 13 | 14 | nextpage allows you to bind keys to some nextpage related functions. Here is the default key-bindings: 15 | 16 | 21 | Here is the real code for this default binding: 22 | 23 | (bind "SPC" 'nextpage-maybe) 24 | (bind "n" 'nextpage) 25 | (bind "p" 'history-back) 26 | 27 | You can disable/overwrite built-in bindings and define your own bindings easily. Read the built-in help document with the installed add-on for more information. Click help button in add-on option. 28 | 29 | For example, in user preferences page, 30 | 31 | You can bind p to previous-page: 32 | 33 | (bind "p" 'previous-page) 34 | 35 | You can bind c to copy page title and url: 36 | 37 | (bind "c" 'copy-title-and-url-maybe) 38 | 39 | This add-on is only 47K zipped. 40 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | // uniform sync storage access for firefox (browser.storage.sync) and chrome 2 | // (chrome.storage.sync). 3 | /* exported store */ 4 | var store = function () { 5 | "use strict"; 6 | let store; 7 | let storeGet; 8 | let storeSet; 9 | let storeRemove; 10 | let storeClear; 11 | 12 | try { 13 | // firefox 14 | store = browser.storage.sync; 15 | storeGet = function (key, succCallback, errCallback) { 16 | store.get(key).then(succCallback, errCallback); 17 | }; 18 | storeSet = function (keyValues, succCallback, errCallback) { 19 | store.set(keyValues).then(succCallback, errCallback); 20 | }; 21 | storeRemove = function (keys, succCallback, errCallback) { 22 | store.remove(keys).then(succCallback, errCallback); 23 | }; 24 | storeClear = function (succCallback, errCallback) { 25 | store.clear().then(succCallback, errCallback); 26 | }; 27 | } catch (e) { 28 | // chrome 29 | store = chrome.storage.sync; 30 | 31 | /** 32 | * wrap succCallback and errCallback to be a single callback. 33 | */ 34 | let wrapForChrome = function (succCallback, errCallback) { 35 | return function (...args) { 36 | if (chrome.runtime.lastError) { 37 | return errCallback(chrome.runtime.lastError); 38 | } 39 | succCallback(...args); 40 | }; 41 | }; 42 | 43 | storeGet = function (key, succCallback, errCallback) { 44 | store.get(key, wrapForChrome(succCallback, errCallback)); 45 | }; 46 | storeSet = function (keyValues, succCallback, errCallback) { 47 | store.set(keyValues, wrapForChrome(succCallback, errCallback)); 48 | }; 49 | storeRemove = function (keys, succCallback, errCallback) { 50 | store.remove(keys, wrapForChrome(succCallback, errCallback)); 51 | }; 52 | storeClear = function (succCallback, errCallback) { 53 | store.clear(wrapForChrome(succCallback, errCallback)); 54 | }; 55 | } 56 | return { 57 | /** 58 | * key: the key 59 | * succCallback: called with a js object, the fetched key values. 60 | * errCallback: called with an error object, if fetch failed. 61 | */ 62 | get: storeGet, 63 | 64 | /** 65 | * keyValues: the key and values to set 66 | * succCallback: called when set succeeded 67 | * errCallback: called when there is an error 68 | */ 69 | set: storeSet, 70 | 71 | /** 72 | * keys: the keys to remove 73 | * succCallback: called when keys are removed 74 | * errCallback: called when remove failed 75 | */ 76 | remove: storeRemove, 77 | 78 | /** 79 | * succCallback: called when all keys are removed 80 | * errCallback: called when clear failed 81 | */ 82 | clear: storeClear, 83 | }; 84 | }(); 85 | -------------------------------------------------------------------------------- /misc/test-regexp.js: -------------------------------------------------------------------------------- 1 | // -*- mode: js -*- 2 | 3 | // this pattern matches the following cases 4 | // 1. just "next" or "next page"; 5 | // or those wrapped between HTML tag delimiters (e.g. >next<). 6 | // 2. "next" or those wrapped between HTML tag delimiters; 7 | // "next" <1-2 space> right arrow symbol; 8 | // "next" + image suffix (.gif/png etc). 9 | // 3. just a single right arrow, in unicode or HTML entity. 10 | // 4. "下一页" "下页" etc 11 | // 5. "Next Chapter" "Thread Next" etc 12 | // 6. endswith " »" 13 | const nextPattern = /(?:(^|>)(next[ _]page|Avanti|Pagina successiva|التالअगला|ي|आगे|다음|다음 페이지|次へ|Далее|Следующая страница|Próximo|Próxima página|Siguiente|Página siguiente|Weiter|Nächste Seite|Suivant|(la)? page suivante|следующей страницы)(<|$)|(^|>\s*)(next( +page)?|nächste|Suivant|Следующая)(\s*<|$|( | |\u00A0){1,2}?(?:→|›|▸|»|›|>>|&(gt|#62|#x3e);)|1?\.(?:gif|jpg|png|webp))|^(→|›|▸|»|››| ?(&(gt|#62|#x3e);)+ ?)$|(下|后)一?(?:页|糗事|章|回|頁|张)|^(Next Chapter|Thread Next|Go to next page|Next Topic)|( | )»[ \t\n]*$)/i; 14 | 15 | const goodMatch = [ 16 | "next", "Next", "next page", "next_page", "Weiter", 17 | "next", "next page", 18 | "next", "next", 19 | 20 | "next →", "next ›", "next »", "next >", "Next >>>", 21 | "Next >", "Next >", "Next >", "Next >", 22 | "next.gif", "next1.png", "next.webp", "Next »", "Next Page »", 23 | 24 | "››", ">", " >> ", "»", "›", "→", ">", ">", 25 | " »", "URL-based access control\n  »", 26 | "URL-based access control\n  »\n ", 27 | "next >>", "next\u00A0>>", 28 | "2. Getting Started »", 29 | "2. Getting Started\t»", 30 | 31 | "下一页", "下页", "下一章", "下一页 >", 32 | "Thread Next", 33 | "Next Chapter", 34 | "Next Chapter >", 35 | "Go to next page", 36 | "Next Topic", 37 | 38 | "\n 下一页\n ", 39 | 40 | "... Next ", // google search 41 | "Next Page\"arrow\"", 42 | "下一页 »", //verycd 43 | "下一页 »" 44 | ]; 45 | 46 | const badMatch = [ 47 | "on next chapter we will", 48 | "nextit", 49 | "nextpage", 50 | "next time you", 51 | "who is next", 52 | "who cares who is next anyway" 53 | ]; 54 | 55 | let msgs = []; 56 | 57 | let log = function (msg) { 58 | msgs.push(msg); 59 | if (typeof console !== 'undefined') { 60 | console.log(msg); 61 | } else if (typeof print !== 'undefined') { 62 | print(msg); 63 | } 64 | }; 65 | 66 | goodMatch.forEach(function (v) { 67 | if (! nextPattern.test(v)) { 68 | log("should catch: " + v); 69 | } 70 | }); 71 | 72 | badMatch.forEach(function (v) { 73 | if (nextPattern.test(v)) { 74 | log("should not catch: " + v); 75 | } 76 | }); 77 | 78 | (function () { 79 | /** 80 | * return True if href is link to top level index page or same level index 81 | * page. 82 | */ 83 | let hrefIsLinkToIndexPage = function (href) { 84 | return (href.match(/^(\/?|\.\/)index\....l?$/i) || href.match(/^\/$/i)); 85 | }; 86 | 87 | const goodMatch = ["/", 88 | "/index.html", "/index.htm", "/index.php", 89 | "index.html", "index.htm", "index.php", 90 | "./index.html", "./index.htm", "./index.php"]; 91 | const badMatch = ["/tutorial/index.html"]; 92 | goodMatch.forEach(function (v) { 93 | if (! hrefIsLinkToIndexPage(v)) { 94 | log("should catch: " + v); 95 | } 96 | }); 97 | 98 | badMatch.forEach(function (v) { 99 | if (hrefIsLinkToIndexPage(v)) { 100 | log("should not catch: " + v); 101 | } 102 | }); 103 | })(); 104 | 105 | // ====================== 106 | 107 | function extractMTeamId(url) { 108 | const match = url.match(/\.example\.[a-z]*\/detail\/([0-9]+)/i); 109 | if (match) { 110 | return match[1]; 111 | } 112 | } 113 | const url = "https://www.example.com/detail/810433"; 114 | if (extractMTeamId(url) !== "810433") { 115 | msgs.push("extract m-team ID failed"); 116 | } 117 | 118 | // ====================== 119 | 120 | if (msgs.length === 0) { 121 | log("all pass."); 122 | } else { 123 | if (typeof exit !== 'undefined') { 124 | exit(1); 125 | } else if (typeof process !== 'undefined') { 126 | process.exit(1); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | nextpage add-on help 7 | 19 | 20 | 21 | 22 |

Nextpage Add-on Help

23 |

Overview

24 |

Nextpage allows you to configure hotkeys and enable/disable features via preferences page. If you sign in your browser, the configuration is synchronized across devices.

25 |

Notes to old users (nextpage v2.x)

26 |

In nextpage v3.x, the default keybinding for p key has changed from history-back to previous-page, which is considered a more useful default binding. If you prefer the old behavior, add this in nextpage user config: (bind "p" 'history-back)

27 |

Notes to old users (nextpage v1.x)

28 |

If you have used user config file before nextpage v2.0, you need to copy your config to the user config area in add-on preferences. The old config file ~/.config/nextpage.lisp is no longer read by nextpage after v2.0 when nextpage is rewritten in Web Extension.

29 |

About User Config

30 |

You may open nextpage add-on options to see the built-in config and user config. The built-in config defines default bindings and is always active. It is shipped with each version of nextpage and you can't edit it. The user config is a property stored in firefox storage.sync or chrome storage.sync depending on your browser. User config is empty by default. User config can be used to overwrite built-in config. You may remove or modify default bindings defined in built-in config or add new bindings. The user config is synchronized across devices if you have signed in firefox, chrome, or msedge.

31 | 32 |

After you edit user config, click "Save" button to save it. New config will be effective in new tabs or new document opened in existing tabs.

33 | 34 |

The config file has a lisp-like syntax. Lines started with ; are comments and they are ignored by the parser. To temporarily disable a command, you may comment a line by adding ";;" at the beginning of that line.

35 |

Each command must be put in a separate line, because I haven't implemented an sexp parser yet.

36 |

Supported Commands

37 |
38 |
(bind KEY-NAME COMMAND-NAME)
39 |
Bind a key to a command. The syntax for KEY-NAME and all possible COMMAND-NAME is shown in later section.
40 | 41 |
(unbind-all)
42 |
Remove all previous bindings before this command, including built-in bindings.
If you don't want to use any of the built-in key bindings, add this to the top of your user config file.
43 | 44 |
(ignore-on WEBSITE-REGEXP)
45 |
Do not run any nextpage commands when page URL matched WEBSITE-REGEXP.
46 | 47 |
(disable-prefetch)
48 |
By default, nextpage will prerender (chrome/msedge) or prefetch (firefox) nextpage link to speed up page loading. If you disable-prefetch, these won't happen.
49 |
50 |

Debug related commands:

51 |
52 |
(enable-debug)
53 |
more debug info shown in console
54 |
(enable-debug-for-key-events)
55 |
show more info for key events
56 |
(enable-debug-goto-next-page)
57 |
show more info for finding next page links or buttons
58 |
(enable-debug-for-prefetch)
59 |
show more prefetch related info
60 |
61 | 62 |

Built-in Config

63 |

This serves an example of how to write a user config.

64 |
(bind "SPC" 'nextpage-maybe)
 65 | (bind "n" 'nextpage)
 66 | (bind "p" 'history-back)
 67 | 
68 |

Keys are wrapped with double quotes, command is prefixed with single quote. This just mimic emacs (global-set-key (kbd "C-c U") 'rename-uniquely) key-bindings.

69 |

Here are some optional key bindings that is provided in older versions of nextpage. You may add them in your user config file if you like.

70 |
(bind "1" 'history-back)
 71 | (bind "2" 'nextpage)
 72 | (bind "M-p" 'history-back)
 73 | (bind "M-n" 'nextpage)
 74 | 
75 |

Some user config examples:

76 |
;; don't do anything key binding handling on this website
 77 | (ignore-on "https://.*\.example\.com/")
 78 | 
 79 | ;; bind p to previous-page instead of the default history-back
 80 | ;; this is already the default in nextpage v3.x
 81 | (bind "p" 'previous-page)
 82 | 
 83 | ;; allow mouse scroll to goto next page when at bottom of page
 84 | (bind "<wheel-down>" 'nextpage-maybe)
 85 | 
 86 | ;; press C key to copy-title-and-url
 87 | (bind "c" 'copy-title-and-url)
 88 |     
89 | 90 |

Bind Command

91 |

Bind command has a form of (bind KEY-NAME COMMAND-NAME). Now I will describe the two parameters.

92 |
KEY-NAME: keyboard and mouse button notation
93 |

A subset of Emacs key names are used in bind. Here is a table to get you started.

94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 |
Key NameMeaning
akey A
AShift-A
C-aCtrl-A
M-aAlt-A
M-AAlt-Shift-A
C-M-aCtrl-Alt-A
SPCSpace
<f11>F11
<C-S-f11>Ctrl-Shift-F11
<left>Left Arrow
<right>Right Arrow
<wheel-up>scroll wheel up
<wheel-down>scroll wheel down
<mouse-4>mouse backward (on some mouse only)
<mouse-5>mouse forward (on some mouse only)
112 |

Check the source code of this add-on to know exactly which keys are supported. Report a bug if the key you want to use is not supported by nextpage.

113 |

Key names are case sensitive. Key names are typed with double quotes in bind command. e.g. "M-n".

114 |

If you have emacs installed, to known a key's key name (the string representation), start emacs and type C-h c, then type your key. The key name will be shown in minibuffer. Note that key sequence is not supported by nextpage. So you can't bind "C-x n" to nextpage command.

115 |

For more document, see emacs document Keys, Commands and Key Bindings.

116 | 117 |
COMMAND-NAME: All Supported Commands
118 |

Available commands to use with bind:

119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
CommandDescription
nextpagegoto next page
nextpage-maybescroll down. If already at page bottom, goto next page.
previous-pagegoto previous page
previous-page-maybescroll up. if already at page top, goto previous page. This is not as useful as nextpage-maybe.
history-backgo back one step in history
copy-title-and-url-maybecopy page title and url to clipboard if no text is being selected
copy-title-and-urlcopy page title and url to clipboard
copy-titlecopy page title to clipboard
scroll-upscroll up one page, to read previous content
scroll-downscroll down one page, to read following content
copy-download-linkcopy direct download link for major item on supported websites
nildo nothing
134 |

Commands should be quoted in bind command. For command foo, use single quote form 'foo. Currently omitting the quote also works, but this may change if I choose to introduce a full lisp parser in the future.

135 |

About the nil command: You can disable a key-binding by binding the same key to nil. For example, (bind "SPC" 'nil) will disable the built-in Space binding.

136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | /* global store */ 2 | (function(){ 3 | 'use strict'; 4 | const STORAGE_KEY_USER_CONFIG = 'user-config-text'; 5 | const STORAGE_KEY_PARSED_CONFIG = 'user-config-parsed'; 6 | const DEFAULT_CONFIG_TEXT = '(bind "SPC" \'nextpage-maybe)\n' + 7 | '(bind "n" \'nextpage)\n' + 8 | '(bind "p" \'previous-page)\n'; 9 | 10 | const logTextarea = document.getElementById("log"); 11 | /** 12 | * append log message into textarea 13 | */ 14 | let log = function (msg) { 15 | logTextarea.value += msg + '\n'; 16 | }; 17 | // eslint-disable-next-line no-unused-vars 18 | let clearLog = function () { 19 | logTextarea.value = ''; 20 | }; 21 | let setLog = function (msg) { 22 | logTextarea.value = msg + '\n'; 23 | }; 24 | 25 | /** 26 | * generic error handler 27 | */ 28 | let onError = function (error) { 29 | // eslint-disable-next-line no-console 30 | console.log(error); 31 | }; 32 | 33 | /** 34 | * load user config from add-on storage API and show it in UI. 35 | * this does not make the config effective. 36 | * use reloadUserConfig if you want to reload user config. 37 | */ 38 | let showUserConfig = function () { 39 | store.get( 40 | STORAGE_KEY_USER_CONFIG, 41 | (result) => { 42 | document.getElementById('user-config').value = 43 | result[STORAGE_KEY_USER_CONFIG] || ""; 44 | }, onError); 45 | }; 46 | 47 | const VALID_COMMANDS = ["nextpage-maybe", 48 | "nextpage", 49 | "previous-page", 50 | "previous-page-maybe", 51 | "history-back", 52 | "close-tab", 53 | "copy-title", 54 | "copy-title-and-url", 55 | "copy-title-and-url-maybe", 56 | "disable-prefetch", 57 | "scroll-up", 58 | "scroll-down", 59 | "copy-download-link", 60 | "nil"]; 61 | /** 62 | * returns true if given nextpage config command is valid. 63 | */ 64 | // this is used for syntax checking. 65 | let isValidCommand = function (command) { 66 | return VALID_COMMANDS.indexOf(command) !== -1; 67 | }; 68 | 69 | /** 70 | * parse nextpage user config. currently only bind is supported. 71 | * using dumb regexp to do parsing. sexp read style parsing not supported. 72 | * one bind per line. 73 | * 74 | * comments and empty lines are skipped. 75 | * 76 | * @return { 77 | * bindings: {key-name: command-name}, 78 | * variables: {debug: true|false, debugXXX: true|false}, 79 | * logs: ["warning and error messages"], 80 | * ok: true|false 81 | * } 82 | */ 83 | let parseUserConfig = function (userConfig) { 84 | var noerror = true; 85 | var variables = {}; 86 | var bindings = { 87 | "n": "nextpage", 88 | "p": "history-back", 89 | "SPC": "nextpage-maybe" 90 | }; 91 | var logs = []; 92 | var line_index; 93 | var log = function (msg) { 94 | logs.push('line ' + (line_index + 1) + ': ' + msg); 95 | }; 96 | 97 | var lines = userConfig.split('\n'); 98 | var mo, i; 99 | var line; 100 | var command_pattern = /^\(([a-zA-Z][-a-zA-Z0-9]*)(\s+.*)?\)$/; 101 | var command; //stores first string in sexp list. 102 | for (i = 0; i < lines.length; ++i) { 103 | line_index = i; 104 | line = lines[i].trim(); 105 | if (line === '' || line.match(/^\s*;/)) { 106 | // ignore empty lines and comment lines 107 | continue; 108 | } 109 | if ((mo = command_pattern.exec(line))) { 110 | command = mo[1]; 111 | } else { 112 | log('Error: bad sexp: ' + line); 113 | noerror = false; 114 | continue; 115 | } 116 | 117 | switch (command) { 118 | case "disable-prefetch": 119 | variables['prefetchDisabled'] = true; 120 | break; 121 | case "enable-debug": 122 | variables['debugging'] = true; 123 | break; 124 | case "enable-debug-for-key-events": 125 | variables['debugKeyEvents'] = true; 126 | break; 127 | case "enable-debug-goto-next-page": 128 | variables['debugGotoNextPage'] = true; 129 | break; 130 | case "enable-debug-special-case": 131 | variables['debugSpecialCase'] = true; 132 | break; 133 | case "enable-debug-for-a-tag": 134 | variables['debugATag'] = true; 135 | break; 136 | case "enable-debug-for-domain-check": 137 | variables['debugDomainCheck'] = true; 138 | break; 139 | case "enable-debug-for-content-editable": 140 | variables['debugContentEditable'] = true; 141 | break; 142 | case "enable-debug-for-iframe": 143 | variables['debugIFrame'] = true; 144 | break; 145 | case "enable-debug-for-prefetch": 146 | variables['debugPrefetch'] = true; 147 | break; 148 | case "disable-debug": 149 | variables['debugging'] = false; 150 | break; 151 | case "unbind-all": 152 | // clear all bindings 153 | bindings = {}; 154 | break; 155 | case "ignore-on": 156 | // modifies variables['ignoreOnWebsites'] 157 | (function(){ 158 | let ignoreOnPattern = /\(ignore-on\s+"(.*)"\s*\)/; 159 | let mo = ignoreOnPattern.exec(line); 160 | if (! mo) { 161 | log('Error: ignore-on: not well formed: ' + line); 162 | noerror = false; 163 | return; 164 | } 165 | let website = mo[1]; 166 | if (variables['ignoreOnWebsites']) { 167 | variables['ignoreOnWebsites'].push(website); 168 | } else { 169 | variables['ignoreOnWebsites'] = [website]; 170 | } 171 | })(); 172 | break; 173 | case "bind": 174 | (function () { 175 | var bind_pattern = /\(bind\s+"(.*)"\s+'?([^'\s]*)\s*\)/; 176 | var mo = bind_pattern.exec(line); 177 | var key, command; 178 | 179 | if (! mo) { 180 | log('Error: bind: not well formed: ' + line); 181 | noerror = false; 182 | return; 183 | } 184 | key = mo[1]; 185 | command = mo[2]; 186 | if (key.indexOf(' ') !== -1) { 187 | log('Warning: bind: key sequence is not supported: ' + 188 | key); 189 | } 190 | if (! isValidCommand(command)) { 191 | log('Error: bind: invalid command: ' + command); 192 | noerror = false; 193 | } 194 | if (bindings.hasOwnProperty(key)) { 195 | if (bindings[key] !== command) { 196 | log('Warning: bind: overwrite existing binding (' + 197 | key + ', ' + bindings[key] + ')'); 198 | } else { 199 | log('Warning: bind: duplicate binding (' + 200 | key + ', ' + bindings[key] + ')'); 201 | } 202 | } 203 | bindings[key] = command; 204 | })(); 205 | break; 206 | default: 207 | log('Error: unknown command: ' + command); 208 | noerror = false; 209 | } 210 | } 211 | return { 212 | bindings: bindings, 213 | logs: logs, 214 | ok: noerror, 215 | variables: variables 216 | }; 217 | }; 218 | 219 | /** 220 | * clear user config. 221 | */ 222 | let clearUserConfig = function () { 223 | store.remove([ 224 | STORAGE_KEY_USER_CONFIG, 225 | STORAGE_KEY_PARSED_CONFIG 226 | ], function () { 227 | setLog("user config removed, using built-in config now"); 228 | }, function (error) { 229 | setLog("remove user config failed: " + error); 230 | }); 231 | }; 232 | 233 | /** 234 | * save user configuration and reload config. 235 | */ 236 | let saveAndReload = function () { 237 | let newUserConfig = document.getElementById('user-config').value; 238 | if (newUserConfig.trimRight() === "") { 239 | // clear user config. use system default config. 240 | clearUserConfig(); 241 | return; 242 | } 243 | let parsedUserConfig = parseUserConfig(newUserConfig); 244 | if (! parsedUserConfig.ok) { 245 | log("parse failed. user config not saved."); 246 | return; 247 | } 248 | store.set({ 249 | [STORAGE_KEY_USER_CONFIG]: newUserConfig, 250 | [STORAGE_KEY_PARSED_CONFIG]: parsedUserConfig 251 | }, function () { 252 | setLog("user config saved"); 253 | }, function (error) { 254 | setLog("save user config failed: " + error); 255 | }); 256 | }; 257 | 258 | let help = function () { 259 | window.open("usage.html"); 260 | }; 261 | 262 | /** 263 | * fill in initial values. 264 | */ 265 | let initUI = function () { 266 | document.getElementById('built-in-config').value = DEFAULT_CONFIG_TEXT; 267 | showUserConfig(); 268 | document.getElementById('save-and-reload').addEventListener('click', saveAndReload); 269 | document.getElementById('help').addEventListener('click', help); 270 | 271 | // I18N 272 | let getMessage = (typeof(browser) === "undefined"? chrome : browser).i18n.getMessage; 273 | 274 | let updateText = function (elementId, messageId) { 275 | document.getElementById(elementId).firstChild.nodeValue = getMessage(messageId); 276 | }; 277 | let labelMapping = [ 278 | ["title", "nextPageAddOnOptions"], 279 | ["built-in-config-label", "builtInConfig"], 280 | ["user-config-label", "userConfig"], 281 | ["save-and-reload", "save"], 282 | ["help", "help"], 283 | ["report-a-bug-intro", "reportBugIntro"], 284 | ["report-a-bug", "reportBug"], 285 | ]; 286 | for (let [elementId, messageId] of labelMapping) { 287 | updateText(elementId, messageId); 288 | } 289 | }; 290 | 291 | initUI(); 292 | })(); 293 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | * COMMENT -*- mode: org -*- 2 | #+Date: 2017-02-24 3 | Time-stamp: <2025-11-27> 4 | 5 | * Rewrite nextpage add-on using WebExtensions 6 | 7 | * Privacy Policy 8 | see ./privacy-policy.rst or on the page you download this add-on. 9 | 10 | * Credit 11 | Icon made by Freepik from www.flaticon.com 12 | 13 | * notes :entry: 14 | ** how to install the extension 15 | For Firefox, 16 | https://addons.mozilla.org/en-US/firefox/addon/nextpage/ 17 | 18 | For Chrome, 19 | https://chrome.google.com/webstore/detail/nextpage/njgkgdihapikidfkbodalicplflciggb?hl=en 20 | 21 | For the new Edge (based on chromium), 22 | https://microsoftedge.microsoft.com/addons/detail/bdgjidjidpokocijgeefmliejkkjannk 23 | 24 | You can also search "nextpage" in Firefox/Chrome/Edge add-on/extension site. 25 | 26 | ** WebExtensions documents 27 | Browser Extensions - Mozilla | MDN 28 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions 29 | 30 | Content scripts - Mozilla | MDN 31 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts 32 | Options page - Mozilla | MDN 33 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Options_pages 34 | storage.sync - Mozilla | MDN 35 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/sync 36 | Publishing your extension - Mozilla | MDN 37 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Publishing_your_WebExtension 38 | Internationalization - Mozilla | MDN 39 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Internationalization 40 | Interact with the clipboard - Mozilla | MDN 41 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard 42 | https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand 43 | 44 | ** Chrome Extension documents 45 | Developer's Guide - Google Chrome 46 | https://developer.chrome.com/extensions/devguide 47 | 48 | JavaScript APIs - Google Chrome 49 | https://developer.chrome.com/extensions/api_index 50 | 51 | chrome.commands - Google Chrome 52 | https://developer.chrome.com/extensions/commands 53 | 54 | Message Passing - Google Chrome 55 | https://developer.chrome.com/extensions/messaging 56 | 57 | ** how to test web extension on localhost? 58 | - Open "about:debugging" in Firefox, click "Load Temporary Add-on" and select 59 | any file in your extension's directory. 60 | 61 | You can also use web-ext to load temporary WebExtensions from the command line. 62 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Getting_started_with_web-ext 63 | this requires node LTS and npm. 64 | 65 | - for Chrome, open Extensions page, click load unpacked extension, choose the 66 | src dir. 67 | 68 | ** how to build extension for distribution? 69 | just run make 70 | 71 | This will create zip files that you can upload to firefox/chrome developer 72 | page. 73 | 74 | ** test pages 75 | Scrapy 1.6.0 documentation 76 | https://docs.scrapy.org/en/latest/intro/overview.html 77 | debian install manual: 78 | http://www.debian.org/releases/stable/amd64/ 79 | emacs manual: 80 | http://www.gnu.org/software/emacs/manual/html_node/emacs/Commands.html#Commands 81 | freebsd handbook: 82 | https://docs.freebsd.org/en/books/handbook/ 83 | boost library: 84 | https://www.boost.org/doc/libs/1_66_0/doc/html/array.html 85 | bing search result: 86 | https://www.bing.com/search?q=debian&qs=n&form=QBLH&sp=-1&pq=debian&sc=8-2&sk=&cvid=B56E4A93A3C4496DA0D9B031F698D9FA 87 | Elastic doc 88 | https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html 89 | PostgreSQL: Documentation: 9.6: Constraints 90 | https://www.postgresql.org/docs/9.6/static/ddl-constraints.html 91 | servant documentation 92 | https://docs.servant.dev/ 93 | bilibili video site search: 94 | https://search.bilibili.com/all?keyword=hehe&from_source=banner_search 95 | lapack Guide 96 | http://www.netlib.org/lapack/lug/node5.html 97 | 98 | ** add-on distribution agreement 99 | Firefox Add-on Distribution Agreement - Mozilla | MDN 100 | https://developer.mozilla.org/en-US/Add-ons/AMO/Policy/Agreement 101 | Add-on Policies - Mozilla | MDN 102 | https://developer.mozilla.org/en-US/Add-ons/AMO/Policy/Reviews 103 | App Developer Agreement | Microsoft Docs 104 | https://docs.microsoft.com/en-us/legal/windows/agreements/app-developer-agreement 105 | 106 | ** how to publish a new version? 107 | - update version in 108 | ./src/manifest-chrome.json 109 | ./src/manifest-firefox.json 110 | - build extension, run 111 | make 112 | - test locally. 113 | - publish nextpage-chrome.zip and nextpage-firefox.zip to web store or dev hub. 114 | 115 | ** distribution URL 116 | Firefox Developer Hub 117 | https://addons.mozilla.org/en-US/developers/ 118 | 119 | Chrome Webstore Developer Dashboard 120 | https://chrome.google.com/webstore/developer/dashboard 121 | click existing item > left hand side, Build > Package > rhs "Upload new package" button 122 | 123 | // 2022-08-29 really bad UI design. old UI is easier to find where to upload new pkg. 124 | 125 | Microsoft Edge 126 | https://partner.microsoft.com/en-us/dashboard/microsoftedge/overview 127 | - select the extension 128 | - Packages > click "Replace" link 129 | - Extension overview > click new version's Publish button to trigger review 130 | 131 | ** in user config, how to add new command for user to bind? 132 | - update config file parser in options.js, namely VALID_COMMANDS 133 | - update command interpreter in nextpage.js, namely runUserCommand 134 | - add command implementation in nextpage.js 135 | - update "Available commands:" in ./src/usage.html 136 | 137 | * waiting :entry: 138 | ** 2018-04-04 known problem: in chrome, Ctrl and Alt hotkeys are not supported in bind config. 139 | in chrome, C-c etc doesn't trigger a keypress event. 140 | in order to use these keys, you need to use commands system. 141 | https://developer.chrome.com/extensions/commands 142 | I don't know whether hotkey key events are exposed in other ways. 143 | ** 2017-10-18 in add-on website, config file help URL is down. 144 | ** 2017-10-19 zh_CN's messages.json is not read by firefox. 145 | not sure why. 146 | * todos :entry: 147 | ** 148 | ** 2025-11-14 translate the extension to more languages. 149 | - title 150 | - description 151 | - preferences UI 152 | - user guide html 153 | 154 | ** 2025-11-14 gh issue Extension Icon Click Support #62 155 | - click extension icon should go to nextpage. 156 | let user config which command to run when user click the icon. 157 | 158 | (bind "icon-click" 'nextpage) ; this is the default 159 | (bind "icon-click" 'copy-title-and-url) 160 | - make click work. then add customize support. 161 | 162 | when you define your own popup or action, the default click action is 163 | override. 164 | 165 | - gpt: can I capture icon click event in content_script? I don't want to 166 | introduce background script. 167 | 168 | nope. 169 | you have to use bg script or service worker. 170 | 171 | I need to break goto-nextpage function to a library/module, so both content 172 | script and service worker script can import the same code. 173 | 174 | //this requires quite a lot of work. 175 | 176 | - 2025-11-27 I think another way is to add an option to add a floating prev 177 | page, next page button on the page. on touch device, you can toggle that in 178 | preferences and get the buttons. 179 | 180 | if I can capture swipe gesture event, that is the best. enable that by 181 | default. then also release the firefox extension for mobile/android. 182 | 183 | swipe to right > prev page 184 | swipe to left > next page 185 | 186 | ** 2023-07-04 make preferences window support dark-mode if browser theme is dark-mode. 187 | ** 2023-07-04 I would like to add a context menu item to copy-title-and-url, 188 | especially when hotkey is disabled on the page. is that possible? 189 | 190 | in user config, you can enable this menu item by 191 | (enable-context-menu 'copy-title-and-url) 192 | 193 | ** 2019-03-07 make getNextPageLink() function usable in browser dev tools. 194 | e.g. 195 | let np = (function() {...})(); 196 | np.linkToString(np.getNextPageLink()); 197 | 198 | - I need to move some function definitions. 199 | what is the best way to define a function? 200 | 201 | function foo() {...} 202 | let foo = function () {...}; 203 | const foo = function () {...}; 204 | 205 | - Functions - JavaScript | MDN 206 | https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions 207 | 208 | Seems it doesn't matter. just use whatever syntax I like. 209 | 210 | - 211 | 212 | ** 2017-10-19 test it on android. is there a swipe event I can bind to? 213 | add a right arrow button on url bar, also add menu item. 214 | 215 | ** 2017-10-19 send notification when next page link is not found. 216 | see example here: 217 | webextensions-examples/background-script.js at master · mdn/webextensions-examples 218 | https://github.com/mdn/webextensions-examples/blob/master/notify-link-clicks-i18n/background-script.js 219 | ** 220 | ** 2021-04-28 mouse-2 (middle button) bindings won't work in new edge in windows. :bug: 221 | middle button triggers auto scroll in edge in windows. 222 | nextpage doesn't get the event. 223 | 224 | https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event 225 | press middle button on the example here doesn't work either. 226 | 227 | * done :entry: 228 | ** 2025-11-14 https://eu.startpage.com/ next page not working in German locale #61 229 | - try this. 230 | LANG=de_DE.UTF-8 ~/bin/chrome --user-data-dir="$HOME/.config/google-chrome-local" 231 | https://eu.startpage.com/ 232 | 233 | no. I can't reproduce this issue. 234 | - in startpage web search settings 235 | startpage language: Deutsch 236 | 237 | yes. I can confirm the bug. now next button is in Germany, and fail in nextpage. 238 | Weiter 239 | 240 | #+begin_quote 241 | 242 | #+end_quote 243 | 244 | that's easy to fix. update the regex. 245 | 246 | - also add support for some other languages. 247 | gpt: can you show me most popular "next" or "next page" phrase in most popular languages across the globe? 248 | 249 | ** 2025-11-14 do not activate nextpage on one-kvm web UI. 250 | test page 251 | https://192.168.2.17/ 252 | also ignore on pi-kvm, iLO, iDrac and other IPMI web UI. 253 | 254 | ** 2023-12-22 fix issue #60 www.google.com should click on "More Results" 255 | - the link 256 | #+begin_quote 257 | 268 | #+end_quote 269 | just add "More results" in link text should work. 270 | 271 | ~/projects/firefox/nextpage-we/misc/test-regexp.js 272 | 273 | but would this match things that it should not match? 274 | maybe just add it in preGeneric rule. 275 | 276 | www.google.com 277 | how about country level google sites? 278 | www.google.fr 279 | #+begin_quote 280 | 291 | #+end_quote 292 | 293 | www.google.com.hk 更多结果 294 | 295 | #+begin_quote 296 | 307 | #+end_quote 308 | 309 | - use preGeneric, I can support CN and EN. 310 | 311 | querySelector match on attribute 312 | it works. 313 | 314 | ** 2023-07-04 distribute on AMO gives a warning about Manifest V3 315 | Manifest V3 compatibility warning 316 | 317 | Firefox is adding support for manifest version 3 (MV3) extensions in Firefox 318 | 109.0, however, older versions of Firefox are only compatible with manifest 319 | version 2 (MV2) extensions. We recommend uploading Manifest V3 extensions as 320 | self-hosted for now to not break compatibility for your users. 321 | 322 | For more information about the MV3 extension roll-out or self-hosting MV3 323 | extensions, visit https://mzl.la/3hIwQXX 324 | 325 | - well. if firefox doesn't retire MV2, I will stay in MV2. 326 | 327 | ** 2023-07-04 switch to manifest v3. 328 | - chrome 329 | Extensions - Chrome Developers 330 | https://developer.chrome.com/docs/extensions/ 331 | Migrate from Manifest V2 to Manifest V3 332 | - v3 changes 333 | - service worker replace background pages. 334 | I don't use bg pages. 335 | - other changes doesn't affect my extension. 336 | // so I can just update version to 3. 337 | - update my extension 338 | - Update the manifest - Chrome Developers 339 | https://developer.chrome.com/docs/extensions/migrating/manifest/ 340 | - Manifest V3 migration checklist - Chrome Developers 341 | https://developer.chrome.com/docs/extensions/migrating/checklist/ 342 | - Replace Browser Actions and Page Actions with Actions 343 | https://developer.chrome.com/docs/extensions/migrating/api-calls/#replace-browser-page-actions 344 | MOVED I would like to add a context menu item to copy-title-and-url, when hotkey 345 | is disabled. is that possible? 346 | 347 | in user config, you can enable this menu item by 348 | (enable-context-menu 'copy-title-and-url) 349 | - yeah, nothing to change for chrome. 350 | - now change firefox's document. 351 | - nothing to change for firefox. also, firefox is not deprecating v2. 352 | just different browser version support different WebExtension APIs. 353 | 354 | Browser extensions - Mozilla | MDN 355 | https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions 356 | manifest.json - Mozilla | MDN 357 | https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json 358 | Browser support for JavaScript APIs - Mozilla | MDN 359 | https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs 360 | 361 | - new code tested in chrome 114. 362 | - test in firefox 114. 363 | - problems 364 | - when firefox is using dark theme, my preferences page doesn't follow the theme. 365 | bg is white. 366 | - hotkeys not working in firefox. 367 | no key events. 368 | check API changes in firefox. 369 | 370 | I used 371 | document.addEventListener("keydown", function (e) {...}) 372 | 373 | Permissions 374 | access your data for all websites toggle is off. 375 | 376 | when this is enabled, it works. 377 | so check permission changes in v3 in firefox. 378 | 379 | search: firefox web extension access your data for all websites is off by default 380 | 381 | I think browser will ask user when user install the add-on. 382 | - in chrome, preferences page width, height should be set. 383 | default is too small. user can't see Save and Help button. 384 | 385 | search: chrome options_ui window min height and width 386 | 387 | try add css in options.html 388 | 389 | nope. min-height is document height, it just add white space at page 390 | bottom. the window size is still small. 391 | 392 | search: chrome options_ui window min height and width 393 | 394 | How can I set height of chrome extension to browser window height? - Stack Overflow 395 | https://stackoverflow.com/questions/47568794/how-can-i-set-height-of-chrome-extension-to-browser-window-height 396 | 397 | 600px height and 800px width - are the maximums for a popup size. 398 | You can't change that. 399 | 400 | Then I need to rework this UI. 401 | 402 | can I show preferences in a regular tab? 403 | 404 | Chrome Extensions: Give users options - Chrome Developers 405 | https://developer.chrome.com/docs/extensions/mv3/options/ 406 | 407 | use options_page to open in a tab. 408 | use options_ui to open in pop up or tab (with open_in_tab: true). 409 | 410 | I will keep using options_page. 411 | Firefox has different behavior with Chrome on this. 412 | 413 | ** 2023-07-04 fix github issue #50, add command for scroll-up and scroll-down 414 | 增加向上、向下滚屏的功能,允许用户定义快捷键 #50 415 | https://github.com/sylecn/ff-nextpage/issues/50 416 | 417 | - Window: scrollBy() method - Web APIs | MDN 418 | https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollBy 419 | 420 | scroll down one page 421 | window.scrollBy(0, window.innerHeight); 422 | 423 | window.scrollBy({top: window.innerHeight, behavior: "instant"}); 424 | 425 | scroll up one page 426 | window.scrollBy(0, -window.innerHeight); 427 | 428 | window.scrollBy({top: -window.innerHeight, behavior: "instant"}); 429 | 430 | window.scrollBy({top: -window.innerHeight, behavior: "auto"}); 431 | // auto is slower. 432 | 433 | page down, page up seems faster. I don't need smooth here. 434 | 435 | - so just add two actions. no default bindings. 436 | 437 | scroll-up 438 | scroll-down 439 | 440 | - also add previous-page-maybe, scroll-up if not at top, previous-page otherwise. 441 | 442 | ** 2023-02-14 nextpage support prefetch 443 | Link prefetching FAQ - HTTP | MDN 444 | https://developer.mozilla.org/en-US/docs/Web/HTTP/Link_prefetching_FAQ 445 | 446 | : The External Resource Link element - HTML: HyperText Markup Language | MDN 447 | https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attributes 448 | firefox doesn't support link prefetch? 449 | 450 | - prefetch is supported by all 3. 451 | prerender is supported by chrome. 452 | 453 | - put this feature behind a flag. disabled by default. 454 | 455 | (enable-prefetch) 456 | (enable-prerender) 457 | 458 | 459 | 460 | 461 | when to add these elements to DOM? after page load. 462 | just add in nextpage.js load time, if feature is enabled. 463 | 464 | - create a test page to test this feature. 465 | https://nextpage.emacsos.com/prefetch-test/1 466 | https://nextpage.emacsos.com/prefetch-test/2 467 | https://nextpage.emacsos.com/prefetch-test/3 468 | 469 | put a lot of text and image on each page. 470 | have next page and prev page link on each page. 471 | 472 | 473 | 474 | - if it works, update command in usage.html. 475 | add notes on prerender is not supported by firefox. 476 | 477 | prefetch works in firefox. 478 | prerender works in chrome/msedge. 479 | 480 | - problems 481 | - in chrome, load unpacked no longer works 482 | I can't see any files, only dir. click open doesn't add local extension. 483 | 484 | drop file from file manager to that page also fail. 485 | "drop to install" shows, but does nothing. 486 | 487 | is it affected by chrome local admin? 488 | /etc/opt/chrome/policies/managed/policy001.json 489 | 490 | xorg/chrome.sls:/etc/opt/chrome/policies/managed/policy001.json 491 | 492 | nope. still fail to install. 493 | 494 | chrome://policy/ 495 | 496 | try drop policy. still fail to install local addon. 497 | 498 | - try it in windows. 499 | in ryzen5 winten02 VM. 500 | 501 | cp -r build/chrome/ ~/d/shared/nextpage-chrome/ 502 | it works in windows 10. 503 | 504 | prerender works very well on freebsd handbook and bing search result 505 | page. I would like to enable it by default. User can disable it via config 506 | file. 507 | 508 | ** 2021-04-26 , , , no longer work. :invalid: 509 | github issue 48 510 | https://github.com/sylecn/ff-nextpage/issues/48 511 | 512 | - mouse-5 no longer work in chrome. 513 | doesn't work in firefox. 514 | enable debug. 515 | oh. I don't have mouse-5 key. I only have 516 | wheel-down works in both chrome and firefox. 517 | - 2021-04-26 test whether (wheel press), (right click) 518 | binding works in windows. 519 | 520 | mouse-2, mouse-3 doesn't work in linux, nor windows. 521 | mouse-2 on linux send no event. 522 | mouse-2 on windows trigger free 4-way scroll. 523 | mouse-3 show context menu on both linux and windows. 524 | - EventTarget.addEventListener() - Web APIs | MDN 525 | https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 526 | Event reference | MDN 527 | https://developer.mozilla.org/en-US/docs/Web/Events 528 | MouseEvent - Web APIs | MDN 529 | https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent 530 | Element: auxclick event - Web APIs | MDN 531 | https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event 532 | 533 | try listen to auxclick event. 534 | test it in both windows (winten01 VM) and linux. 535 | 536 | auxclick works. mouse-2 and mouse-3 event is triggered properly. 537 | 538 | - problems 539 | - auxclick works. mouse-2 and mouse-3. 540 | but when using mouse-3, context menu still shows. 541 | 542 | search: auxclick disable context menu on right click 543 | 544 | - add "return false". 545 | nope. 546 | 547 | - Element: contextmenu event - Web APIs | MDN 548 | https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event 549 | 550 | Any right-click event that is not disabled (by calling the event's 551 | preventDefault() method) will result in a contextmenu event being fired at 552 | the targeted element. 553 | 554 | Element: auxclick event - Web APIs | MDN 555 | https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event 556 | 557 | Additionally, you may need to avoid opening a system context menu after a 558 | right click. Due to timing differences between operating systems, this too 559 | is not a preventable default behavior of auxclick. Instead, this can be 560 | done by preventing the default behavior of the contextmenu event. 561 | 562 | MouseEvent.button - Web APIs | MDN 563 | https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button 564 | 565 | Users may change the configuration of buttons on their pointing device so 566 | that if an event's button property is zero, it may not have been caused by 567 | the button that is physically left–most on the pointing device; however, 568 | it should behave as if the left button was clicked in the standard button 569 | layout. 570 | 571 | - works in firefox. 572 | in chrome, mouse-2 and mouse-3 event not triggered. 573 | 574 | two nextpage installed. 575 | load unpacked extension will not replace the extension installed from webstore. 576 | 577 | try use a chrome profile just for testing this extension. 578 | it works. 579 | 580 | in edge, mouse-2 event not triggered. 581 | mouse-3 works fine. 582 | wheel-down works fine. 583 | 584 | search: middle mouse click doesn't trigger on edge on windows 585 | 586 | Fix Microsoft Edge Mouse Wheel Not Working - Technipages 587 | https://www.technipages.com/fix-microsoft-edge-mouse-wheel-not-working 588 | 589 | search: middle mouse click event new edge browser in windows 590 | search: capture middle mouse click event in new edge browser 591 | 592 | MOVED mouse-2 (middle button) doesn't work in new edge in windows. 593 | I will leave it for now. 594 | 595 | ** 2019-08-23 support previous-page command. 596 | ** 2019-08-23 describeKeyInEmacsNotation bug. shift+p should be "P", not S-p 597 | just like *. 598 | 599 | C-S-l ==> same as C-l 600 | S-l ==> L 601 | M-S-l ==> M-L 602 | 603 | the emacs rules are kind of complicated. 604 | I should add some test case for this. 605 | if control key and letter keys are pressed, ignore and consume shift key. 606 | 607 | - test cases: 608 | | press | emacs notation | 609 | |-------+----------------| 610 | | l | l | 611 | | S-l | L | 612 | | 8 | 8 | 613 | | * | * | 614 | | C-l | C-l | 615 | | C-L | C-l | 616 | | M-l | M-l | 617 | | M-L | M-L | 618 | | C-M-L | C-M-l | 619 | | C-* | C-* | 620 | | C-M-* | C-M-* | 621 | | C-. | C-. | 622 | | C-> | C-> | 623 | 624 | ** 2019-08-22 describeKeyInEmacsNotation() is not reliable. 625 | "C-S-," should be C-< in emacs notation. 626 | keyCode is S-KEYCODE188 627 | all symbols that requires shift key to press have this problem. 628 | "*", not S-8 629 | 630 | - DONE support (bind "C-." 'nextpage) 631 | - DONE support (bind "C-S-b" 'nextpage) 632 | - DONE support (bind "C-(" 'nextpage) 633 | (bind "C-<" 'nextpage) 634 | - DONE support (bind "*" 'nextpage) 635 | 636 | "-" key is 173 in firefox 68.0.2. 637 | "-" key is 189 in chrome 76.0.x. 638 | 639 | e.key will be '-' in both browser. 640 | 641 | use e.key can't support keys, because browser just return "2". so I 642 | continue to use keyCode to support more keys. 643 | - 644 | 645 | ** 2019-08-22 keypress event to keydown event. fix issue #41. 646 | - problems 647 | - press C-i will trigger two events. one for ctrl, one for C-i. 648 | I should ignore pure modifier key events. 649 | I don't plan to support key chain. 650 | - I can capture some built-in hotkey after switching to keydown event. 651 | in chrome, C-o, C-h, C-c all send to my extension. C-n is not sent. 652 | 653 | can I block default behavior by returning false? 654 | - TODO bug in firefox. press p will go previous page, then immediately go next page. 655 | 656 | reproduce: 657 | 658 | open debian install manual. https://www.debian.org/releases/stable/amd64/ 659 | press n 660 | press p 661 | expect see previous page, but see previous page, then immediately see next 662 | page. 663 | 664 | No problem in chrome. 665 | 666 | it's because nextpageLink.click(); send a click. 667 | my ext got that click event. but there is no binding for this key. 668 | 669 | - open debian install manual. 670 | press n 671 | press toolbar back button. has the same issue. 672 | looks like a bug of firefox. 673 | 674 | only debian install manual have the problem? not all page have this 675 | problem. ignore this bug for now. 676 | 677 | - firefox 68 doesn't have the problem. maybe it's firefox profile issue. 678 | - 679 | 680 | ** 2018-04-06 svg icon not supported by chrome webstore. 681 | can't install. 682 | 683 | https://developer.chrome.com/apps/manifest/icons 684 | 685 | - use png icon. 686 | 128x128 most commonly used 687 | 48x48 used in chrome://extensions 688 | 16x16 favicon 689 | 690 | https://www.flaticon.com/free-icon/next-page-hand-drawn-symbol_35480#term=next%20page&page=1&position=3 691 | 692 | ** 2018-04-05 publish extension on chrome webstore 693 | https://chrome.google.com/webstore/developer/dashboard 694 | 695 | - release it now. 696 | 697 | - Detailed description, can I include links? 698 | no. 699 | 700 | - need a few 1280x800 screenshot 701 | 702 | could be any design of that size. doesn't necessarily a screenshot. 703 | 704 | ~/d/nextpage-screenshots/ 705 | 706 | sc example pages in 640x400 707 | 708 | // image quality is very poor on desktop browser. 709 | 710 | #+BEGIN_QUOTE 711 | Applications and themes require at least one screenshot. Extensions may have 712 | no screenshots, but such extensions won't be shown in Chrome Web Store's 713 | browse functions. 714 | 715 | Provide preferably 4 or 5 screenshots of your app (up to a maximum of 5). If 716 | your app supports multiple locales, you can provide locale-specific 717 | screenshots. Your screenshot should have square corners and no padding (full 718 | bleed). 719 | 720 | We prefer screenshots to be 1280x800 pixels in size, as they will be used in 721 | future high dpi displays. Currently, the Chrome Web Store will downscale all 722 | screenshots to 640x400, so if your screenshots will not look good downscaled 723 | (eg. have a lot of text) or if 1280x800 is too big for your app (screenshot 724 | of low resolution game), we also support 640x400 pixels screenshots. 725 | #+END_QUOTE 726 | 727 | For now, I should use 640x400 images. 728 | 729 | just put 2 images, one intro, one options page. 730 | 731 | - should I enable GA for it? 732 | 733 | - A one-time developer registration fee of US$5.00 is required to verify your 734 | account and publish items. Learn more 735 | 736 | paid. 737 | 738 | - publish okay. 739 | 740 | - problems 741 | - I see this notice on developer dashboard. 742 | 743 | As of November 21st, 2016, all newly published packaged or hosted apps are 744 | restricted to Chrome OS, and are not available to users on Windows, Mac or 745 | Linux. 746 | 747 | All packaged and hosted apps will be removed from Chrome Web Store search 748 | & browse functions in mid-December 2017. Existing apps will continue to 749 | work and receive updates. 750 | 751 | it links to 752 | Chromium Blog: From Chrome Apps to the Web 753 | https://blog.chromium.org/2016/08/from-chrome-apps-to-web.html 754 | 755 | //WTF? 756 | 757 | DONE I will read it later. if the store doesn't work, just upload to 758 | https://emacsos.com/misc/nextpage-chrome.zip 759 | 760 | this only affects chrome apps. is web extension one kind of chrome apps? 761 | nope. it's for apps like atom editor, which is a desktop app that is built 762 | on top of chromium. 763 | 764 | - 765 | 766 | ** 2018-04-04 feature request: allow bind copy-title-and-url-maybe. 767 | this is for myself. not in default config. 768 | I can remove the tabcopy add-on and copy fixer add-on when this is done. 769 | 770 | (bind "C-c" 'copy-title-and-url-maybe) 771 | 772 | make it work in both firefox and chrome. 773 | 774 | - add command implementation 775 | 776 | add in nextpage.js, 777 | copyTitleAndUrl 778 | copyTitleAndUrlMaybe 779 | 780 | - update config file parser. 781 | in options.js, 782 | update VALID_COMMANDS 783 | 784 | - update interpreter 785 | in nextpage.js, 786 | runUserCommand 787 | 788 | - well, since commands work differently in chrome. 789 | I can bind c instead of Ctrl-C. 790 | 791 | - I will get rid of background script and commands. 792 | 793 | Notes (FYI): to make C-c work in chrome: 794 | 795 | add in manifest-chrome.json: 796 | #+BEGIN_SRC json 797 | "background": { 798 | "scripts": ["bg.js"], 799 | "persistent": false 800 | }, 801 | "commands": { 802 | "copy-title-and-url-maybe": { 803 | "suggested_key": { 804 | "default": "Ctrl+C" 805 | }, 806 | "description": "Copy Title and URL to clipboard when no text is selected" 807 | } 808 | }, 809 | #+END_SRC 810 | 811 | create bg.js, 812 | #+BEGIN_SRC js 813 | (function () { 814 | // eslint-disable-next-line no-console 815 | let log = console.log; 816 | 817 | if (typeof(chrome) !== 'undefined' && 818 | typeof(chrome.commands) !== 'undefined') { 819 | chrome.commands.onCommand.addListener(function(command) { 820 | switch (command) { 821 | case "copy-title-and-url": 822 | case "copy-title-and-url-maybe": 823 | chrome.tabs.query({ 824 | active: true, 825 | currentWindow: true 826 | }, function(tabs) { 827 | chrome.tabs.sendMessage(tabs[0].id, {"onCommand": command}); 828 | }); 829 | break; 830 | default: 831 | log('command not supported:', command); 832 | } 833 | }); 834 | } 835 | })(); 836 | #+END_SRC 837 | 838 | add in nextpage.js, 839 | #+BEGIN_SRC js 840 | // use command system to handle combination keys 841 | if (typeof(chrome) !== 'undefined' && 842 | typeof(chrome.runtime) !== 'undefined') { 843 | chrome.runtime.onMessage.addListener( 844 | // eslint-disable-next-line no-unused-vars 845 | function(request, sender, sendResponse) { 846 | if (typeof(request.onCommand) === 'undefined') { 847 | return; 848 | } 849 | let command = request.onCommand; 850 | switch (command) { 851 | case "copy-title-and-url": 852 | copyTitleAndUrl(); 853 | break; 854 | case "copy-title-and-url-maybe": 855 | copyTitleAndUrlMaybe(); 856 | break; 857 | default: 858 | log("unknown command: ", command); 859 | } 860 | }); 861 | } 862 | #+END_SRC 863 | 864 | - problems 865 | - how to check whether some text is selected? 866 | I think I have this code somewhere. 867 | 868 | var selection = window.getSelection(); 869 | 870 | - how to copy text to clipboard? 871 | 872 | just check how TabCopy do it. 873 | 874 | ~/.config/google-chrome/Default/Extensions/micdllihgoppmejpecmkilggmaagfdmb/ 875 | copyToClipboard() 876 | 877 | it modify doc.oncopy to manually overwrite clipboardData, then call 878 | doc.execCommand('copy') so the oncopy event handler is run. 879 | 880 | this is mentioned in 881 | https://developer.mozilla.org/en-US/docs/Web/Events/copy 882 | 883 | - TODO bug: when saving user config, no warning given when there is unknown 884 | command. 885 | 886 | - in chrome, C-c doesn't trigger runUserCommand() at all. 887 | 888 | (enable-debug-for-key-events) 889 | 890 | no keypress event when press C-c in chrome. 891 | 892 | Since I can't reuse the infrastructures, I will create a separate 893 | extension for this. 894 | 895 | How about firefox, does it support this? 896 | it works in firefox. 897 | 898 | how to know C-c is pressed in chrome? TabCopy can do it. 899 | 900 | https://developer.chrome.com/extensions/events 901 | which events are available in chrome? most apis. 902 | 903 | https://developer.chrome.com/extensions/api_index 904 | commands API. 905 | 906 | commands only work with background script (event pages). 907 | need to send command event to content script to copy page's title and url. 908 | (in bg.js, it will get bg.js's document url, I tried it.) 909 | 910 | works now. although I didn't see the default hotkey shown anywhere in 911 | chrome UI. 912 | 913 | https://developer.chrome.com/extensions/commands 914 | The user can manually add more shortcuts from the 915 | chrome://extensions/configureCommands dialog. 916 | (Extensions > Keyboard Shortcuts) 917 | 918 | - how to communicate between background script and content script? 919 | https://developer.chrome.com/extensions/messaging 920 | 921 | - C-c doesn't work in options.html textarea, when it's used to copy-title-and-url-maybe. 922 | 923 | problem went away after I removed bg.js and commands. 924 | I don't need those anyway. 925 | 926 | ** 2018-04-04 bug: in firefox, doc link doesn't link to help page in preferences page. 927 | - see also the bug in waiting section. that's mozilla's add-on website. 928 | - in chrome, there is a "Help" button, which opens the help page. 929 | 930 | // this is not a problem. there is a help button on both firefox and chrome. 931 | 932 | ** 2018-03-13 make it work in chrome browser. 933 | - create a new manifest.json for chrome. drop some unsupported keys. 934 | - make browser.storage.sync optional in nextpage.js and options.js 935 | or use chrome's sync object. 936 | - I tried it on x201 today. n key already works. SPC key doesn't work. 937 | 938 | - 2018-04-04 continue this. 939 | I designed the firefox extension first. 940 | I will focus on 941 | - how to sync user configuration across sessions. 942 | 943 | https://developer.chrome.com/extensions/options 944 | 945 | Use the storage.sync API to persist these preferences. These values will 946 | then become accessible in any script within your extension, on all your 947 | user's devices. 948 | 949 | chrome.storage.sync.set(data, callback); 950 | chrome.storage.sync.get(keyWithDefaultValues, callback); 951 | 952 | - fix any bugs. 953 | - nextpage.js:905 browser is not defined 954 | nextpage.js:979 955 | 956 | https://developer.chrome.com/extensions/storage#type-StorageArea 957 | StorageArea api is not the same. 958 | in firefox, .get return a Promise. 959 | in chrome, you pass in a callback function. 960 | 961 | wait, I used .then() api in firefox. not callback. 962 | do I need to create a wrapper for storage access? 963 | 964 | search: chrome.storage.sync browser.storage.sync wrapper 965 | 966 | seems none exists. I will create one, just for my needs. 967 | 968 | That's just reading user's parsed config. fixed. 969 | 970 | - preferences page doesn't show up. 971 | update manifest.json, key is different. 972 | 973 | - fix storage usage in options.html 974 | options.js:7, :273 975 | 976 | I split store code in storage.js 977 | I need to load/include this file in both nextpage.js and options.js. 978 | 979 | fix store usage in options.js 980 | 981 | - fix browser.i18n.getMessage in options.js 982 | document.getElementById(elementId).firstChild.nodeValue = browser.i18n.getMessage(messageId); 983 | 984 | - all test pages work in chrome now. 985 | test this build in firefox dev env. 986 | make sure I didn't break anything. 987 | 988 | all test page works in firefox. 989 | 990 | - problems 991 | - try load the unpacked addon. 992 | - manifest.json 993 | - Unrecognized manifest key 'applications'. 994 | - found unexpected key 'browser_style' 995 | search: one manifest.json file for both chrome and firefox extension 996 | 997 | I will use jinja2 template. 998 | Can I use m4 for this? 999 | 1000 | Since there are only two browsers, and manifest file is not updated very 1001 | frequently, I will just use two files. 1002 | 1003 | DONE update build process to use manifest-chrome.json for chrome browser. 1004 | - how to pack for chrome? 1005 | https://developer.chrome.com/extensions/hosting 1006 | it's just zip file, same as firefox. 1007 | DONE I will create a build dir since manifest file is different. 1008 | - after split storage.js, I listed two js files in content_scripts js. 1009 | but the later js file can't access the variables defined in first js file. 1010 | 1011 | https://developer.chrome.com/extensions/content_scripts#execution-environment 1012 | 1013 | only page script and content script is isolated. 1014 | doesn't say anything about content scripts in one extension. 1015 | if js files in each content script is isolated, there is no use to include 1016 | jquery as an example there. 1017 | 1018 | Are content scripts supposed to share a single global namespace? - Google Groups 1019 | https://groups.google.com/forum/#!topic/chromium-extensions/-xdbFNr7Q2I 1020 | 1021 | #+BEGIN_QUOTE 1022 | All content scripts for the same extension/page pair should share a 1023 | context. If you aren't seeing this, it is a bug. 1024 | #+END_QUOTE 1025 | 1026 | it's my problem. use var to define global variable. 1027 | now it works. 1028 | - in chrome, SPC key doesn't goto next page when at page bottom. 1029 | n key does work. 1030 | 1031 | window.scrollMaxY <= window.scrollY 1032 | this is always false in chrome. 1033 | 1034 | window.scrollMaxY is undefined in chrome, so it's always <= a number. 1035 | scrollMaxY is not standard. 1036 | 1037 | search: detect scrolled to bottom 1038 | https://stackoverflow.com/questions/9439725/javascript-how-to-detect-if-browser-window-is-scrolled-to-bottom 1039 | (window.innerHeight + window.scrollY) >= document.body.offsetHeight 1040 | 1041 | fixed. 1042 | - doesn't goto next page on debian site. 1043 | try enable debugging. 1044 | (enable-debug) 1045 | (enable-debug-goto-next-page) 1046 | 1047 | it finds the next page link, but failed to follow the link. 1048 | href=https://www.debian.org/releases/stable/amd64/pr01.html.en 1049 | 1050 | maybe .click() is not supported on this node in chrome. 1051 | 1052 | test page: 1053 | https://www.debian.org/releases/stable/amd64/ 1054 | var head = document.getElementsByTagName('head'); 1055 | var lastElement = head[0].lastElementChild; 1056 | lastElement 1057 | lastElement.click() 1058 | the click() function is defined, but it doesn't goto that URL in chrome. 1059 | 1060 | document.location.href = lastElement.href; 1061 | 1062 | (lastElement.tagName.toUpperCase() === "LINK") 1063 | 1064 | fixed. 1065 | - 1066 | 1067 | ** 2017-10-18 changelog for first 2.x release 1068 | - [feature] support firefox 57+ 1069 | - [feature] user configuration is now synced by browser if you logged in. 1070 | - [feature] add new user config command ignore-on, you can now disable 1071 | nextpage on some websites. fix issue #15. 1072 | - [notice] This version is a rewrite in Web Extension API. There are two 1073 | changes that may affect user experience. 1074 | - User config file ~/.config/nextpage.lisp is obsolete. If you have created 1075 | user config file, please copy & paste the text to the new options window. 1076 | - If some resource of a page loads very slow, nextpage hotkeys may not work 1077 | on them. 1078 | - Please report a bug if you have other issues in the new version. 1079 | - it's approved right away. maybe WE add-on doesn't require much review. 1080 | - test it in xp VM. 1081 | it works. 1082 | - 1083 | - problems 1084 | - how to release the new add-on. it's no longer an xpi. 1085 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Publishing_your_WebExtension 1086 | just zip it. 1087 | 1088 | - DONE what is update_url? I don't have it yet. 1089 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/applications 1090 | try remove it. 1091 | - warns about it's one-way upgrade. you can not go back to xpi. 1092 | 1093 | TODO I need to check my js files for errors and unused code. 1094 | maybe rename some variables with underscore in them. 1095 | 1096 | TODO localization is not added yet. Chinese users will lose UI. 1097 | 1098 | - validator doesn't like usage of .innerHTML 1099 | how to update h3 and span in javascript without using innerHTML? 1100 | 1101 | search: update span without using innerHTML 1102 | using innerText 1103 | or textContent 1104 | 1105 | myDiv.childNodes[0].nodeValue = "The text has been changed."; 1106 | 1107 | document.getElementById('elementid').firstChild.nodeValue = new_text; 1108 | 1109 | ** 2017-10-19 run jslint on my js files 1110 | make check 1111 | 1112 | ** 2017-10-19 implement I18N for options window and manifest file. 1113 | 1114 | - problems 1115 | - do I need to use javascript to rewrite HTML tags in options.html? 1116 | that will be boring. 1117 | 1118 | search: firefox web extension option page I18N 1119 | 1120 | title and labels shows nothing. 1121 | add-on name is also not reflected on UI. 1122 | 1123 | try restart firefox 1124 | 1125 | - There was an error during installation: Extension is invalid 1126 | 1127 | search: firefox There was an error during installation: Extension is invalid 1128 | 1129 | works when I add messages.json for zh_CN. maybe it doesn't like empty 1130 | locale dirs. 1131 | 1132 | now i18n works. 1133 | 1134 | - how to check zh_CN locale options page. 1135 | about:config 1136 | general.useragent.locale 1137 | set to zh-CN 1138 | need to restart firefox. 1139 | 1140 | addon name and description works. 1141 | options page still show English. 1142 | options page only fetch English message.json, not other languages. 1143 | confirm in zh-CN and zh locale, it doesn't fetch _locale/zh_CN/messages.json 1144 | 1145 | search: browser.i18n.getMessage always fetch English 1146 | 1147 | ** 2017-10-18 disabled website 可以开放出来了。fix bug #15 1148 | (ignore-on "https://www.qidian.com/") 1149 | (ignore-on "https://.*\.qidian\.com/") 1150 | 1151 | both works. cool. 1152 | 1153 | - parse this command in options.js 1154 | 1155 | how to do proper parsing? there may be escapes in the string. 1156 | just use regexp for now. don't care escapes. 1157 | 1158 | - use the parse result in nextpage.js 1159 | - document it in usage.html 1160 | 1161 | ** 2017-10-18 add an options page. store user hotkeys somewhere. 1162 | - make user config file backward compatible. 1163 | - ~/projects/firefox/nextpage/src/chrome/content/config.jsm 1164 | - the old preferences window 1165 | config file: ~/.config/nextpage.lisp 1166 | 1167 | built-in config: 1168 | (bind "SPC" 'nextpage-maybe) 1169 | (bind "n" 'nextpage) 1170 | (bind "p" 'history-back) 1171 | 1172 | user config: 1173 | 1174 | 1175 | [save & reload] [reload only] [Help] 1176 | - just remake it using WE api. 1177 | Options page - Mozilla | MDN 1178 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Options_pages 1179 | 1180 | reading a local text file is impossible now. 1181 | maybe for security reasons. 1182 | I will need to store the user configuration somewhere else. 1183 | 1184 | just use add-on storage api. 1185 | https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Storage 1186 | 1187 | parse result: 1188 | see parseUserConfig docstring. 1189 | 1190 | make sure this works: 1191 | 1. open webpage. n, p, space works. 1192 | 2. change user config. 1193 | #+BEGIN_SRC lisp 1194 | ;; user config 1195 | (enable-debug) 1196 | (bind "SPC" 'nextpage-maybe) 1197 | (bind "M-n" 'nextpage) 1198 | (bind "p" 'history-back) 1199 | #+END_SRC 1200 | save and reload. 1201 | 3. open new webpage. M-n, p, SPC works. 1202 | 4. in old webpage. default key works. M-n doesn't. 1203 | - make mouse click work. 1204 | - if user clear user config, clear the parsed config key. 1205 | - update src/usage.html, there is no longer "file" concept. 1206 | 1207 | To retain old behavior, if user doesn't say unbind-all, all built-in binding 1208 | should continue work. 1209 | - 1210 | 1211 | - problems 1212 | - how should the two communicate? content script and options page? 1213 | I can just read key map from storage api. this way it's always latest. 1214 | when reload, just parse user config and store it in another key. 1215 | 1216 | notify page script to update a lexical binding is better. 1217 | especially those debug variables. 1218 | I don't like reading from pref for all of them. 1219 | 1220 | It will make things too complicated. Just make config work in new 1221 | tabs. Leave existing tabs alone. 1222 | - DONE remove all occurrence of in_overlay checks 1223 | - storage saved in browser.storage.sync can't get back. 1224 | storage.local has the same problem. 1225 | I can't get back what I put in. 1226 | 1227 | I see the problem. 1228 | let setKey = store.set({ 1229 | STORAGE_KEY_USER_CONFIG: newUserConfig, 1230 | STORAGE_KEY_PARSED_CONFIG: parsedUserConfig 1231 | }); 1232 | the capital name becomes object literal, not constant! 1233 | you can force evaluation of the const using [CONST_NAME]. 1234 | 1235 | works now. 1236 | - 1237 | 1238 | ** DONE 2017-10-18 make basic things work. 1239 | 2017-10-18 16:46:15 basic things work. 1240 | test whether there are any js errors, then make a commit. 1241 | 1242 | - problems 1243 | - can I put my code in multiple files and compile them to one? 1244 | I want to use es2015 modules. 1245 | 1246 | search: can I use es2015 modules in firefox web extension 1247 | 1248 | search: compile es2015 modules to a single javascript file 1249 | 1250 | I will keep it simple. just use old javascript. 1251 | 1252 | - DONE search for content\. after migrating. content is not needed in WE. 1253 | 1254 | -------------------------------------------------------------------------------- /src/nextpage.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021, 2022, 2023, 2024, 2025 Yuanle Song 2 | // 3 | // The JavaScript code in this page is free software: you can 4 | // redistribute it and/or modify it under the terms of the GNU 5 | // General Public License (GNU GPL) as published by the Free Software 6 | // Foundation, either version 3 of the License, or (at your option) 7 | // any later version. The code is distributed WITHOUT ANY WARRANTY; 8 | // without even the implied warranty of MERCHANTABILITY or FITNESS 9 | // FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. 10 | // 11 | // You should have received a copy of the GNU General Public License 12 | // along with this program. If not, see . 13 | // 14 | // As additional permission under GNU GPL version 3 section 7, you 15 | // may distribute non-source (e.g., minimized or compacted) forms of 16 | // that code without the copy of the GNU GPL normally required by 17 | // section 4, provided you include this license notice and a URL 18 | // through which recipients can access the Corresponding Source. 19 | 20 | /* global store */ 21 | (function () { 22 | 'use strict'; 23 | let i; 24 | let variables = {}; 25 | 26 | let debugging = function () {return variables.debugging;}; 27 | let debugKeyEvents = function () {return variables.debugKeyEvents;}; 28 | let debugGotoNextPage = function () {return variables.debugGotoNextPage;}; 29 | let debugSpecialCase = function () {return variables.debugSpecialCase;}; 30 | let debugATag = function () {return variables.debugATag;}; 31 | let debugDomainCheck = function () {return variables.debugDomainCheck;}; 32 | let debugContentEditable = function () { 33 | return variables.debugContentEditable; 34 | }; 35 | let debugIFrame = function () {return variables.debugIFrame;}; 36 | let debugPrefetch = function () {return variables.debugPrefetch;}; 37 | /** 38 | * return true if prefetch is enabled. 39 | */ 40 | let prefetchEnabled = function () {return ! variables.prefetchDisabled;}; 41 | // eslint-disable-next-line no-console 42 | let log = console.log; 43 | 44 | if (typeof KeyEvent === "undefined") { 45 | var KeyEvent = { 46 | DOM_VK_CANCEL: 3, 47 | DOM_VK_HELP: 6, 48 | DOM_VK_BACK_SPACE: 8, 49 | DOM_VK_TAB: 9, 50 | DOM_VK_CLEAR: 12, 51 | DOM_VK_RETURN: 13, 52 | DOM_VK_ENTER: 14, 53 | DOM_VK_SHIFT: 16, 54 | DOM_VK_CONTROL: 17, 55 | DOM_VK_ALT: 18, 56 | DOM_VK_PAUSE: 19, 57 | DOM_VK_CAPS_LOCK: 20, 58 | DOM_VK_ESCAPE: 27, 59 | DOM_VK_SPACE: 32, 60 | DOM_VK_PAGE_UP: 33, 61 | DOM_VK_PAGE_DOWN: 34, 62 | DOM_VK_END: 35, 63 | DOM_VK_HOME: 36, 64 | DOM_VK_LEFT: 37, 65 | DOM_VK_UP: 38, 66 | DOM_VK_RIGHT: 39, 67 | DOM_VK_DOWN: 40, 68 | DOM_VK_PRINTSCREEN: 44, 69 | DOM_VK_INSERT: 45, 70 | DOM_VK_DELETE: 46, 71 | DOM_VK_0: 48, 72 | DOM_VK_1: 49, 73 | DOM_VK_2: 50, 74 | DOM_VK_3: 51, 75 | DOM_VK_4: 52, 76 | DOM_VK_5: 53, 77 | DOM_VK_6: 54, 78 | DOM_VK_7: 55, 79 | DOM_VK_8: 56, 80 | DOM_VK_9: 57, 81 | DOM_VK_SEMICOLON: 59, 82 | DOM_VK_EQUALS: 61, 83 | DOM_VK_A: 65, 84 | DOM_VK_B: 66, 85 | DOM_VK_C: 67, 86 | DOM_VK_D: 68, 87 | DOM_VK_E: 69, 88 | DOM_VK_F: 70, 89 | DOM_VK_G: 71, 90 | DOM_VK_H: 72, 91 | DOM_VK_I: 73, 92 | DOM_VK_J: 74, 93 | DOM_VK_K: 75, 94 | DOM_VK_L: 76, 95 | DOM_VK_M: 77, 96 | DOM_VK_N: 78, 97 | DOM_VK_O: 79, 98 | DOM_VK_P: 80, 99 | DOM_VK_Q: 81, 100 | DOM_VK_R: 82, 101 | DOM_VK_S: 83, 102 | DOM_VK_T: 84, 103 | DOM_VK_U: 85, 104 | DOM_VK_V: 86, 105 | DOM_VK_W: 87, 106 | DOM_VK_X: 88, 107 | DOM_VK_Y: 89, 108 | DOM_VK_Z: 90, 109 | DOM_VK_CONTEXT_MENU: 93, 110 | DOM_VK_NUMPAD0: 96, 111 | DOM_VK_NUMPAD1: 97, 112 | DOM_VK_NUMPAD2: 98, 113 | DOM_VK_NUMPAD3: 99, 114 | DOM_VK_NUMPAD4: 100, 115 | DOM_VK_NUMPAD5: 101, 116 | DOM_VK_NUMPAD6: 102, 117 | DOM_VK_NUMPAD7: 103, 118 | DOM_VK_NUMPAD8: 104, 119 | DOM_VK_NUMPAD9: 105, 120 | DOM_VK_MULTIPLY: 106, 121 | DOM_VK_ADD: 107, 122 | DOM_VK_SEPARATOR: 108, 123 | DOM_VK_SUBTRACT: 109, 124 | DOM_VK_DECIMAL: 110, 125 | DOM_VK_DIVIDE: 111, 126 | DOM_VK_F1: 112, 127 | DOM_VK_F2: 113, 128 | DOM_VK_F3: 114, 129 | DOM_VK_F4: 115, 130 | DOM_VK_F5: 116, 131 | DOM_VK_F6: 117, 132 | DOM_VK_F7: 118, 133 | DOM_VK_F8: 119, 134 | DOM_VK_F9: 120, 135 | DOM_VK_F10: 121, 136 | DOM_VK_F11: 122, 137 | DOM_VK_F12: 123, 138 | DOM_VK_F13: 124, 139 | DOM_VK_F14: 125, 140 | DOM_VK_F15: 126, 141 | DOM_VK_F16: 127, 142 | DOM_VK_F17: 128, 143 | DOM_VK_F18: 129, 144 | DOM_VK_F19: 130, 145 | DOM_VK_F20: 131, 146 | DOM_VK_F21: 132, 147 | DOM_VK_F22: 133, 148 | DOM_VK_F23: 134, 149 | DOM_VK_F24: 135, 150 | DOM_VK_NUM_LOCK: 144, 151 | DOM_VK_SCROLL_LOCK: 145, 152 | DOM_VK_COMMA: 188, 153 | DOM_VK_PERIOD: 190, 154 | DOM_VK_SLASH: 191, 155 | DOM_VK_BACK_QUOTE: 192, 156 | DOM_VK_OPEN_BRACKET: 219, 157 | DOM_VK_BACK_SLASH: 220, 158 | DOM_VK_CLOSE_BRACKET: 221, 159 | DOM_VK_QUOTE: 222, 160 | DOM_VK_META: 224 161 | }; 162 | } 163 | 164 | /** 165 | * return true if user is typing, e.g. when active element is input/ta 166 | * etc. 167 | */ 168 | let userIsTyping = function () { 169 | let focusElement = document.activeElement; 170 | 171 | // walk down the frames to get the bottom level activeElement 172 | while (focusElement.tagName.match(/^FRAME$/i)) { 173 | focusElement = focusElement.contentDocument.activeElement; 174 | } 175 | 176 | // get active element inside shadow DOM 177 | // https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/activeElement 178 | // walk into current shadow dom's activeElement until it is not a shadow dom. 179 | while (focusElement.shadowRoot) { 180 | focusElement = focusElement.shadowRoot.activeElement; 181 | } 182 | 183 | if (focusElement.tagName.match(/^(INPUT|TEXTAREA|SELECT)$/i)) { 184 | return true; 185 | } 186 | if (debugContentEditable()) { 187 | log(focusElement.tagName + 188 | "\nfocusElement.contentEditable=" + 189 | focusElement.contentEditable); 190 | } 191 | // when contentEditable is set to true, a BODY tag or DIV tag will 192 | // become editable, so treat them just like other input controls. 193 | if (focusElement.contentEditable === "true") { 194 | return true; 195 | } 196 | // IFRAME is a also an input control when inner document.designMode is 197 | // set to "on". Some blog/webmail rich editor use IFRAME instead of 198 | // TEXTAREA. 199 | if (debugIFrame()) { 200 | if (focusElement.tagName === "IFRAME") { 201 | log(focusElement.tagName + 202 | "\nfocusElement.contentEditable=" + 203 | focusElement.contentEditable + 204 | "\ndocument.designMode=" + 205 | focusElement.contentDocument.designMode + 206 | "\nbody.contentEditable=" + 207 | focusElement.contentDocument.body.contentEditable); 208 | } 209 | } 210 | // Note: some website is using IFRAME for textarea, but designMode is 211 | // not set to "on", including: gmail, qq mail. I don't know how they 212 | // make that work. 213 | if (focusElement.tagName === "IFRAME" && 214 | (focusElement.contentDocument.designMode.toLowerCase() === "on" || 215 | focusElement.contentDocument.body.contentEditable)) { 216 | return true; 217 | } 218 | 219 | return false; 220 | }; 221 | 222 | /** 223 | * Some websites use the same hotkeys as nextpage. To prevent nextpage 224 | * from capturing the hotkeys, add the website and key binding they 225 | * use in this alist. 226 | * 227 | * the key of the alist is a regexp that matches to document URL. 228 | * the value of the alist is a list of keys to ignore. 229 | * 230 | * the key of the alist can be a literal string as well, which will be 231 | * converted to regexp by calling new RegExp(str). 232 | * 233 | * nextpage will stop when it finds the first match, so you should put 234 | * more specific regexp earlier in the list. 235 | */ 236 | let ignoreBindingAList = [ 237 | [/https:\/\/germanoid\.github\.io\/terminal-helper\//i, "*"], 238 | [/https?:\/\/www\.google\.com\/reader\/view/i, ['SPC', '1', '2']], 239 | [/https?:\/\/www\.google\.com\/transliterate/i, "*"], 240 | [/http:\/\/typing.sjz.io\//i, "*"], 241 | [/https:\/\/qwerty.kaiyi.cool\//i, "*"], 242 | // one-kvm, pi-kvm 243 | [/https:\/\/.*\/kvm\//i, "*"], 244 | // exception rule, pipermail or mailing list archives is not webmail. 245 | [/mail\..*\/(pipermail|archives)/i, ""], 246 | // ignore common webmail hosts, nextpage bindings can do little on 247 | // these domains. 248 | [/\W(web)?mail\.[^.]+\.(com|org|net|edu)/i, "*"] 249 | ]; 250 | 251 | /** 252 | * return true if this key should be ignored on current website. 253 | * return false otherwise. 254 | */ 255 | let shouldIgnoreKey = function (key) { 256 | const url = utils.getURL(); 257 | for (let v of ignoreBindingAList) { 258 | if (url.match(v[0])) { 259 | if (v[1] === "") { 260 | // user explicitly says do not ingore any key 261 | return false; 262 | } 263 | if (v[1] === "*" || utils.inArray(key, v[1])) { 264 | if (debugging()) { 265 | log("ignore " + key + " for " + v[0]); 266 | } 267 | return true; 268 | } 269 | return false; 270 | } 271 | } 272 | return false; 273 | }; 274 | 275 | /** 276 | * return true if nextpage should ignore keypress events for this website. 277 | * some website has special key handing. some websites just don't need 278 | * nextpage. 279 | */ 280 | let skipWebsite = function (e) { 281 | // ignore keyevents in XUL, only catch keyevents in content. 282 | if (e.target["namespaceURI"] === 283 | "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") { 284 | return true; 285 | } 286 | let ignoreOnWebsites = variables.ignoreOnWebsites; 287 | if (ignoreOnWebsites) { 288 | const url = utils.getURL(); 289 | for (let i = 0; i < ignoreOnWebsites.length; ++i) { 290 | if (url.match(ignoreOnWebsites[i])) { 291 | if (debugging()) { 292 | log("ignore on " + ignoreOnWebsites[i]); 293 | } 294 | return true; 295 | } 296 | } 297 | } 298 | return false; 299 | }; 300 | 301 | let utils = { 302 | /** 303 | * given a page url, return page number as a number if it is 304 | * found. return null if page can't be found in given URL or its value 305 | * is not a number. 306 | * 307 | * supported URL format: 308 | * ?page=N (query string) 309 | */ 310 | parsePageFromURL: function (pageURL) { 311 | var pagePattern = /[?&]page=([^&]+)/; 312 | var mo = pagePattern.exec(pageURL); 313 | var result; 314 | if (mo) { 315 | result = parseInt(mo[1]); 316 | if (isNaN(result)) { 317 | return null; 318 | } else { 319 | return result; 320 | } 321 | } else { 322 | return null; 323 | } 324 | }, 325 | 326 | /** 327 | * copy given text to clipboard. requires firefox 22, chrome 58. 328 | */ 329 | copyToClipboard: function (text) { 330 | let copyTextToClipboard = function (e) { 331 | e.preventDefault(); 332 | e.clipboardData.setData('text/plain', text); 333 | }; 334 | try { 335 | document.addEventListener('copy', copyTextToClipboard); 336 | try { 337 | document.execCommand('copy'); 338 | } catch (e) { 339 | // eslint-disable-next-line no-console 340 | log("copy to clipboard failed: " + e); 341 | } 342 | } finally { 343 | document.removeEventListener('copy', copyTextToClipboard); 344 | } 345 | }, 346 | 347 | /** 348 | * copy given text to clipboard. if text doesn't end with newline, add 349 | * a newline. 350 | */ 351 | copyToClipboardWithNewLine: function (text) { 352 | const s = text.endsWith("\n") ? text : text + "\n"; 353 | this.copyToClipboard(s); 354 | }, 355 | 356 | /** 357 | * test whether an element is in an array 358 | * @return true if it is. 359 | * @return false otherwise. 360 | */ 361 | inArray: function (element, array) { 362 | var i; 363 | for (i = 0; i < array.length; i++) { 364 | if (element === array[i]) { 365 | return true; 366 | } 367 | } 368 | return false; 369 | }, 370 | 371 | /** 372 | * integer to ASCII 373 | */ 374 | itoa: function (i) { 375 | return String.fromCharCode(i); 376 | }, 377 | 378 | /** 379 | * ASCII to integer 380 | */ 381 | atoi: function (a) { 382 | return a.charCodeAt(); 383 | }, 384 | 385 | /** 386 | * describe mouse click in emacs notation. return a string. 387 | * examples: , , , 388 | */ 389 | describeMouseEventInEmacsNotation: function (e) { 390 | var button = "mouse-" + (e.button + 1); 391 | var ctrl = e.ctrlKey ? "C-": ""; 392 | var meta = (e.altKey || e.metaKey) ? "M-": ""; 393 | var shift = e.shiftKey ? "S-": ""; 394 | var re = '<' + ctrl + meta + shift + button + '>'; 395 | return re; 396 | }, 397 | 398 | /** 399 | * describe wheel event in emacs notation. return a string. 400 | * example: , 401 | * 402 | * Since emacs doesn't support wheel events, I made up wheel-up, 403 | * wheel-down names. 404 | */ 405 | describeWheelEventInEmacsNotation: function (e) { 406 | let direction = ""; 407 | if (e.deltaY < 0) { 408 | direction = "-up"; 409 | } else if (e.deltaY > 0) { 410 | direction = "-down"; 411 | } else if (e.deltaX < 0) { 412 | direction = "-left"; 413 | } else if (e.deltaX > 0) { 414 | direction = "-right"; 415 | } 416 | const wheel = "wheel" + direction; 417 | const ctrl = e.ctrlKey ? "C-": ""; 418 | const meta = (e.altKey || e.metaKey) ? "M-": ""; 419 | const shift = e.shiftKey ? "S-": ""; 420 | const re = '<' + ctrl + meta + shift + wheel + '>'; 421 | return re; 422 | }, 423 | 424 | /** 425 | * return true if keyCode is a pure modifier key. 426 | */ 427 | isPureModifierKey: function (keyCode) { 428 | return (keyCode === KeyEvent.DOM_VK_SHIFT || 429 | keyCode === KeyEvent.DOM_VK_CONTROL || 430 | keyCode === KeyEvent.DOM_VK_ALT); 431 | }, 432 | 433 | /** 434 | * describe key pressed in emacs notation. return a string. 435 | * examples: n, N, C-a, M-n, SPC, DEL, , , C-M-n 436 | * , , C-M-*, M-S-RET, , 437 | * C-., C-<, *, 438 | * @param e a KeyEvent 439 | * @return a string that describes which key was pressed. 440 | */ 441 | describeKeyInEmacsNotation: function (e) { 442 | if (debugKeyEvents()) { 443 | log("keyCode=" + e.keyCode + ", key=" + e.key); 444 | } 445 | 446 | /** 447 | * convert keyCode to emacs key name. ignore modifier keys. only 448 | * convert the keys that doesn't require bracket in emacs 449 | * notation, most commonly printable characters. 450 | * 451 | * Return: 452 | * [keyName, consumeShift?] if this keyCode doesn't require bracket. 453 | * null otherwise. 454 | */ 455 | var getNameForKeyCodeNoBracket = function (e) { 456 | // try get keyName while consuming shift key. 457 | // if get keyName okay, return keyName, otherwise, return null. 458 | let getKeyNameConsumeShift = function (e) { 459 | let keyCode = e.keyCode; 460 | 461 | if (keyCode >= KeyEvent.DOM_VK_A && 462 | keyCode <= KeyEvent.DOM_VK_Z) { 463 | return e.ctrlKey ? e.key.toLowerCase() : e.key; 464 | } 465 | 466 | if (keyCode >= KeyEvent.DOM_VK_0 && 467 | keyCode <= KeyEvent.DOM_VK_9) { 468 | return e.key; 469 | } 470 | switch (keyCode) { 471 | case KeyEvent.DOM_VK_BACK_QUOTE: return "~"; 472 | // special handling for - key in main keyboard area. 473 | case 173: return e.key; 474 | case 189: return e.key; 475 | case KeyEvent.DOM_VK_EQUALS: return "+"; 476 | case KeyEvent.DOM_VK_OPEN_BRACKET: return "{"; 477 | case KeyEvent.DOM_VK_CLOSE_BRACKET: return "}"; 478 | case KeyEvent.DOM_VK_BACK_SLASH: return "|"; 479 | case KeyEvent.DOM_VK_SEMICOLON: return ":"; 480 | case KeyEvent.DOM_VK_QUOTE: return "\""; 481 | case KeyEvent.DOM_VK_COMMA: return "<"; 482 | case KeyEvent.DOM_VK_PERIOD: return ">"; 483 | case KeyEvent.DOM_VK_SLASH: return "?"; 484 | default: return null; 485 | } 486 | }; 487 | 488 | // try get keyName when shift key is not pressed. 489 | let getKeyNameNoShift = function (e) { 490 | let keyCode = e.keyCode; 491 | if ((keyCode >= KeyEvent.DOM_VK_A && keyCode <= KeyEvent.DOM_VK_Z) 492 | || (keyCode >= KeyEvent.DOM_VK_0 && keyCode <= KeyEvent.DOM_VK_9)) { 493 | return String.fromCharCode(keyCode).toLowerCase(); 494 | } 495 | switch (keyCode) { 496 | case KeyEvent.DOM_VK_SPACE: return "SPC"; 497 | case KeyEvent.DOM_VK_SEMICOLON: return ";"; 498 | case KeyEvent.DOM_VK_EQUALS: return "="; 499 | case KeyEvent.DOM_VK_COMMA: return ","; 500 | case KeyEvent.DOM_VK_PERIOD: return "."; 501 | case KeyEvent.DOM_VK_SLASH: return "/"; 502 | case KeyEvent.DOM_VK_BACK_QUOTE: return "`"; 503 | case KeyEvent.DOM_VK_OPEN_BRACKET: return "["; 504 | case KeyEvent.DOM_VK_BACK_SLASH: return "\\"; 505 | case KeyEvent.DOM_VK_CLOSE_BRACKET: return "]"; 506 | case KeyEvent.DOM_VK_QUOTE: return "'"; 507 | 508 | default: return null; 509 | } 510 | }; 511 | 512 | if (e.shiftKey) { 513 | let keyName = getKeyNameConsumeShift(e); 514 | if (keyName !== null) { 515 | return [keyName, true]; 516 | } 517 | } 518 | // even if shiftKey is pressed, it may not be consumed, such 519 | // as C-S-y. 520 | let keyName = getKeyNameNoShift(e); 521 | if (keyName !== null) { 522 | return [keyName, false]; 523 | } 524 | return null; 525 | }; 526 | /** 527 | * convert keyCode to emacs key name. ignore modifier 528 | * keys. convert the keys that requires bracket in emacs 529 | * notation. most commonly function keys and control keys. 530 | */ 531 | var getNameForKeyCodeBracket = function (keyCode) { 532 | switch (keyCode) { 533 | case KeyEvent.DOM_VK_TAB: return "tab"; 534 | 535 | case KeyEvent.DOM_VK_INSERT: return "insert"; 536 | case KeyEvent.DOM_VK_DELETE: return "delete"; 537 | case KeyEvent.DOM_VK_HOME: return "home"; 538 | case KeyEvent.DOM_VK_END: return "end"; 539 | case KeyEvent.DOM_VK_PAGE_UP: return "prior"; 540 | case KeyEvent.DOM_VK_PAGE_DOWN: return "next"; 541 | 542 | case KeyEvent.DOM_VK_BACK_SPACE: return "backspace"; 543 | case KeyEvent.DOM_VK_ESCAPE: return "escape"; 544 | 545 | case KeyEvent.DOM_VK_LEFT: return "left"; 546 | case KeyEvent.DOM_VK_UP: return "up"; 547 | case KeyEvent.DOM_VK_RIGHT: return "right"; 548 | case KeyEvent.DOM_VK_DOWN: return "down"; 549 | 550 | case KeyEvent.DOM_VK_RETURN: return "RET"; 551 | case KeyEvent.DOM_VK_CONTEXT_MENU: return "menu"; 552 | case KeyEvent.DOM_VK_MULTIPLY: return "kp-multiply"; 553 | case KeyEvent.DOM_VK_ADD: return "kp-add"; 554 | case KeyEvent.DOM_VK_SUBTRACT: return "kp-subtract"; 555 | case KeyEvent.DOM_VK_DECIMAL: return "kp-decimal"; 556 | case KeyEvent.DOM_VK_DIVIDE: return "kp-divide"; 557 | 558 | case KeyEvent.DOM_VK_F1: return "f1"; 559 | case KeyEvent.DOM_VK_F2: return "f2"; 560 | case KeyEvent.DOM_VK_F3: return "f3"; 561 | case KeyEvent.DOM_VK_F4: return "f4"; 562 | case KeyEvent.DOM_VK_F5: return "f5"; 563 | case KeyEvent.DOM_VK_F6: return "f6"; 564 | case KeyEvent.DOM_VK_F7: return "f7"; 565 | case KeyEvent.DOM_VK_F8: return "f8"; 566 | case KeyEvent.DOM_VK_F9: return "f9"; 567 | case KeyEvent.DOM_VK_F10: return "f10"; 568 | case KeyEvent.DOM_VK_F11: return "f11"; 569 | case KeyEvent.DOM_VK_F12: return "f12"; 570 | 571 | default: 572 | if (keyCode >= KeyEvent.DOM_VK_NUMPAD0 && 573 | keyCode <= KeyEvent.DOM_VK_NUMPAD9) { 574 | return "kp-" + (keyCode - KeyEvent.DOM_VK_NUMPAD0); 575 | } else { 576 | return "KEYCODE" + keyCode; 577 | } 578 | } 579 | }; 580 | 581 | var noWrap = true; // whether to wrap key with <> 582 | var shiftIsConsumed = false; // whether shift key is consumed 583 | // in keyName. 584 | var keyName = ""; 585 | var r = getNameForKeyCodeNoBracket(e); 586 | if (r === null) { 587 | noWrap = false; 588 | keyName = getNameForKeyCodeBracket(e.keyCode); 589 | } else { 590 | keyName = r[0]; 591 | shiftIsConsumed = r[1]; 592 | } 593 | var ctrl = e.ctrlKey ? "C-": ""; 594 | var meta = (e.altKey || e.metaKey) ? "M-": ""; 595 | var shift = e.shiftKey && (! shiftIsConsumed) ? "S-": ""; 596 | var re = ctrl + meta + shift + keyName; 597 | if (! noWrap) { 598 | re = '<' + re + '>'; 599 | } 600 | return re; 601 | }, 602 | 603 | /** 604 | * @return current page's URL as a string. 605 | */ 606 | getURL: function (win) { 607 | if (! win) { 608 | win = window; 609 | } 610 | return win.location.toString(); 611 | }, 612 | 613 | /** 614 | * @return tag with given name. in jQuery syntax: 615 | * $("meta[name=$name]").attr("content") 616 | * @return false if the name is not found. 617 | */ 618 | getMeta: function (name, doc) { 619 | var i; 620 | if (! doc) { 621 | doc = window.document; 622 | } 623 | var metas = doc.getElementsByTagName("meta"); 624 | for (i = 0; i < metas.length; ++i) { 625 | if (metas[i].getAttribute("name") === name) { 626 | return metas[i].getAttribute("content"); 627 | } 628 | } 629 | return false; 630 | }, 631 | 632 | // convert anchor (link) object to string 633 | linkToString: function (l) { 634 | let re = "link = <" + l.tagName + "> {\n"; 635 | let prop = ["rel", "accessKey", "title", "href", "onclick", 636 | "innerHTML", "id", "name"]; 637 | for (let i = 0; i < prop.length; i++) { 638 | if (l.hasAttribute(prop[i])) { 639 | re += prop[i] + ": " + l.getAttribute(prop[i]) + ",\n"; 640 | } 641 | } 642 | return re + "}"; 643 | } 644 | }; 645 | 646 | // ================================ 647 | // special cases for some website 648 | // ================================ 649 | 650 | /* 651 | * hook functions for special cases defined in getNextPageLink's 652 | * preGeneric and postGeneric. 653 | * 654 | * all hook functions are called with two arguments: url and doc. 655 | * hook function should return false if no link is found, otherwise, 656 | * it should return the link object. 657 | */ 658 | 659 | // ADD new preGeneric and postGeneric handler here 660 | 661 | /** 662 | * www.google.com, www.google.com.hk, www.google.fr etc. 663 | */ 664 | let getLinkForGoogleSearch = function (url, doc) { 665 | // query selector can not match on multiple attribute values, so I use 666 | // multiple css selector ORed together to match on any of the 667 | // translation of "More results" text. 668 | return doc.querySelector('a[aria-label="More results"],a[aria-label="更多结果"],a[aria-label="更多結果"],a[aria-label="Weitere Ergebnisse"]'); 669 | }; 670 | 671 | /** 672 | * example wordpress.com page https://socket3.wordpress.com/blog/page/4/ 673 | */ 674 | let getLinkForWordPress = function (url, doc) { 675 | return doc.querySelector('div[class~="nav-previous"] a'); 676 | }; 677 | 678 | /** 679 | * the PKU BBS next page link look like this: 680 | *
下一页 >
681 | */ 682 | let getLinkForPkuBBS = function (url, doc) { 683 | let divs = doc.querySelectorAll('div[class~="paging-button"]'); 684 | for (let div of divs) { 685 | if (div.lastChild.textContent === "下一页 >") { 686 | return div.firstChild; 687 | } 688 | } 689 | return false; 690 | }; 691 | 692 | let getLinkForDockerHub = function (url, doc) { 693 | return doc.querySelector('div[class~="dpagination"] li[class*="styles__nextPage"]'); 694 | }; 695 | 696 | // ninenines hosts doc for a few erlang libraries. 697 | // example url: 698 | // https://ninenines.eu/docs/en/cowboy/2.3/guide/getting_started/ 699 | let getLinkForNinenines = function (url, doc) { 700 | let navNodes = doc.getElementsByTagName("nav"); 701 | for (let navNode of navNodes) { 702 | if (navNode.getAttribute("style") !== "margin:1em 0") { 703 | continue; 704 | } 705 | let links = navNode.getElementsByTagName("a"); 706 | for (let link of links) { 707 | if (link.getAttribute("style") === "float:right") { 708 | return link; 709 | } 710 | } 711 | return false; 712 | } 713 | return false; 714 | }; 715 | 716 | let getLinkForDiscuz = function (url, doc) { 717 | var generator; 718 | var className; 719 | if (window.discuzVersion == "X2") { 720 | className = "nxt"; 721 | } else { 722 | generator = utils.getMeta("generator"); 723 | if (! generator) { 724 | return false; 725 | } 726 | if (generator.match(/^Discuz! X/)) { 727 | className = "nxt"; 728 | } else if (generator.match(/^Discuz! /)) { 729 | className = "next"; 730 | } else { 731 | return false; 732 | } 733 | } 734 | var nodes = doc.getElementsByClassName(className); 735 | if (nodes.length < 1) { 736 | return false; 737 | } 738 | return nodes[0]; 739 | }; 740 | 741 | let getLinkForOsdirML = function (url, doc) { 742 | // last in div.osDirPrevNext. I wish I have jQuery at my disposal. 743 | // $("div.osDirPrevNext > a:last") 744 | var nodes = doc.getElementsByClassName("osDirPrevNext"); // FF3 only. 745 | if (nodes.length < 1) { 746 | return false; 747 | } 748 | var links = nodes[0].getElementsByTagName("a"); 749 | var link = links[links.length - 1]; 750 | // /**/log('innerHTML' + link.innerHTML); 751 | if (link.innerHTML === ">>") { 752 | return link; 753 | } 754 | return false; 755 | }; 756 | 757 | let getLinkForDerkeilerML = function (url, doc) { 758 | // when there is a ul.links element, locate it first. then find li 759 | // node that contains "Next (in|by) thread:", then search in this node 760 | // for a link. see bug #139, #208. 761 | var nodes = doc.getElementsByClassName("links"); 762 | var links; 763 | if (nodes.length > 0) { 764 | nodes = nodes[0].getElementsByTagName("li"); 765 | } else { 766 | nodes = doc.getElementsByTagName("li"); 767 | } 768 | for (i = 0; i < nodes.length; ++i) { 769 | if (nodes[i].innerHTML.match(/Next (in|by) thread:/)) { 770 | links = nodes[i].getElementsByTagName("a"); 771 | if (links.length > 0) { 772 | return links[0]; 773 | } 774 | } 775 | } 776 | return false; 777 | }; 778 | 779 | let getLinkForWikiSource = function (url, doc) { 780 | var nodes = doc.getElementsByTagName("td"); 781 | var links; 782 | for (i = 0; i < nodes.length; ++i) { 783 | if (nodes[i].innerHTML.match(/→/)) { 784 | links = nodes[i].getElementsByTagName("a"); 785 | if (links.length > 0) { 786 | return links[0]; 787 | } 788 | } 789 | } 790 | return false; 791 | }; 792 | 793 | let getLinkForOpenstackDoc = function (url, doc) { 794 | var inode = doc.getElementsByClassName("fa-angle-double-right"); 795 | if (inode.length < 1) { 796 | return false; 797 | } 798 | return inode[0].parentElement; 799 | }; 800 | 801 | let getLinkForBaiduSearch = function (url, doc) { 802 | // locate A tag with class="n" 803 | var nodes = doc.getElementsByClassName("n"); 804 | if (nodes.length < 1) { 805 | return false; 806 | } 807 | for (i = 0; i < nodes.length; ++i) { 808 | if (nodes[i].innerHTML === "下一页>") { 809 | return nodes[i]; 810 | } 811 | } 812 | return false; 813 | }; 814 | 815 | /** 816 | * @return true if given string matches one of the words that's 817 | * equivalent to 'next'. 818 | * @return false otherwise. 819 | */ 820 | let matchesNext = function (str) { 821 | // str could be null 822 | if (! str) return false; 823 | str = str.trim(); 824 | // str could be space only 825 | if (! str) return false; 826 | 827 | // TODO make this regexp configurable. user config should be named 828 | // extra-next-pattern, or use a function (add-next-pattern PATTERN). 829 | var nextPattern = /(?:(^|>)(next[ _]page|Avanti|Pagina successiva|التالअगला|ي|आगे|다음|다음 페이지|次へ|Далее|Следующая страница|Próximo|Próxima página|Siguiente|Página siguiente|Weiter|Nächste Seite|Suivant|(la)? page suivante|следующей страницы)(<|$)|(^|>\s*)(next( +page)?|nächste|Suivant|Следующая)(\s*<|$|( | |\u00A0){1,2}?(?:→|›|▸|»|›|>>|&(gt|#62|#x3e);)|1?\.(?:gif|jpg|png|webp))|^(→|›|▸|»|››| ?(&(gt|#62|#x3e);)+ ?)$|(下|后)一?(?:页|糗事|章|回|頁|张)|^(Next Chapter|Thread Next|Go to next page|Next Topic)|( | )»[ \t\n]*$)/i; 830 | return nextPattern.test(str) || nextPattern.test(str.slice(1, -1)); 831 | }; 832 | 833 | /** 834 | * @return true if given string matches one of the words that's 835 | * equivalent to 'previous'. 836 | * @return false otherwise. 837 | */ 838 | let matchesPrevious = function (str) { 839 | // str could be null 840 | if (! str) return false; 841 | str = str.trim(); 842 | // str could be space only 843 | if (! str) return false; 844 | 845 | // TODO make this regexp configurable 846 | var previousPattern = /(?:(^|>)(previous[ _]page|Vorherige Seite)(<|$)|(^|>\s*)(prev(ious)?|vorherige|Précédent)(\s*<|$|( | |\u00A0)?(?:←|‹|◂|«|‹|<<|&(lt|#60|#x3c);)|1?\.(?:gif|jpg|png|webp))|^(←|‹|◂|«|‹‹| ?(&(lt|#60|#x3c);)+ ?)$|(上|前)一?(?:页|糗事|章|回|頁|张)|^(Previous Chapter|Thread Previous|Go to previous page)| «[ \t\n]*$)/i; 847 | return previousPattern.test(str) || previousPattern.test(str.slice(1, -1)); 848 | }; 849 | 850 | /** 851 | * @param l an anchor object 852 | * @return true if this element is visible 853 | * @return false otherwise 854 | */ 855 | let isVisible = function (l) { 856 | return l.offsetParent !== null; 857 | }; 858 | 859 | /** 860 | * @param url a url string 861 | * @return true if the url pass the domain check. 862 | * This means the url matches the document domain, or it's a file:// or 863 | * javascript: url. 864 | * @return false otherwise. thus the url failed the domain check. 865 | */ 866 | let checkDomain = function (url) { 867 | if (debugDomainCheck()) { 868 | log("checkDomain " + url); 869 | } 870 | 871 | if (url.match(/^javascript:/i)) { 872 | return true; 873 | } 874 | 875 | // eslint-disable-next-line no-useless-escape 876 | var domainPattern = /^([^:]+):\/\/\/?([^:\/]+)/; 877 | var matchResult = domainPattern.exec(url); 878 | 879 | if (! matchResult) { 880 | // should be a relative link. 881 | return true; 882 | } 883 | if (matchResult[1] === "file") { 884 | return true; 885 | } 886 | if (matchResult[2].indexOf(document.domain) !== -1) { 887 | return true; 888 | } 889 | if (debugDomainCheck()) { 890 | log("domain compare: link at " + matchResult[2] + 891 | ", this doc at " + document.domain); 892 | log("domain check failed."); 893 | } 894 | return false; 895 | }; 896 | 897 | /** 898 | * check link with matchFunc. 899 | * 900 | * @param l an anchor object 901 | * @param matchFunc the checker function. matchFunc signature: 902 | * matchFunc(string) -> boolean. 903 | * @param accessKey single char string, return true if link has this 904 | * accessKey. 905 | * 906 | * @return true if link has text that matches according to matchFunc or 907 | * has given accessKey. false otherwise. 908 | */ 909 | let checkLinkWithFunc = function (l, matchFunc, accessKey) { 910 | /** 911 | * if debugATag is enabled, log message; otherwise, do nothing. 912 | */ 913 | let debugLog = function (msg) { 914 | if (debugATag()) { 915 | log(msg); 916 | } 917 | }; 918 | 919 | // check rel 920 | if (l.hasAttribute("rel")) { 921 | if (matchFunc(l.getAttribute("rel"))) { 922 | // if rel is used, it's usually the right link. GNU info 923 | // html doc is using rel to represent the relation of the 924 | // nodes. 925 | return true; 926 | } 927 | } 928 | 929 | // check accesskey 930 | if (l.getAttribute("accesskey") === accessKey) { 931 | // some well written html already use accesskey n to go to 932 | // next page, in firefox you could just use Alt-Shift-n. 933 | return true; 934 | } 935 | 936 | // invisible tag is usually not the right link to nextpage. 937 | if (! isVisible(l)) { 938 | debugLog("link ignored because it's invisible: " + l.outerHTML); 939 | return false; 940 | } 941 | 942 | if (l.hasAttribute("title")) { 943 | if (matchFunc(l.getAttribute("title"))) { 944 | return true; 945 | } 946 | } 947 | 948 | // if we come here, it's not that clear we get a next page link, so more 949 | // restrict rules apply. 950 | 951 | // check domain 952 | if (l.hasAttribute("href")) { 953 | if (! checkDomain(l.getAttribute("href"))) { 954 | debugLog("link ignored because domain check failed: " + l.outerHTML); 955 | return false; 956 | } 957 | } 958 | 959 | // check innerHTML 960 | if (matchFunc(l.innerHTML)) { 961 | return true; 962 | } 963 | 964 | // check inner tag 965 | let imgMaybe = l.getElementsByTagName("img"); 966 | if (imgMaybe.length !== 0) { 967 | if (matchFunc(imgMaybe[0].getAttribute('alt')) || 968 | matchFunc(imgMaybe[0].getAttribute('name')) || 969 | matchFunc(imgMaybe[0].getAttribute('src'))) { 970 | return true; 971 | } 972 | } 973 | // check inner tag 974 | let spanMaybe = l.getElementsByTagName("span"); 975 | if (spanMaybe.length !== 0) { 976 | if (matchFunc(spanMaybe[0].innerHTML)) 977 | return true; 978 | } 979 | 980 | debugLog("link check failed because neither text nor access key matched: " + l.outerHTML); 981 | return false; 982 | }; 983 | 984 | /** 985 | * @param l an anchor object 986 | * @return true if this anchor is link to previous page 987 | * @return false otherwise 988 | */ 989 | let isPreviousPageLink = function (l) { 990 | return checkLinkWithFunc(l, matchesPrevious, 'p'); 991 | }; 992 | 993 | /** 994 | * @param l an anchor object 995 | * @return true if this anchor is link to next page 996 | * @return false otherwise 997 | */ 998 | let isNextPageLink = function (l) { 999 | return checkLinkWithFunc(l, matchesNext, 'n'); 1000 | }; 1001 | 1002 | /** 1003 | * @param b a