├── webextensions ├── extlib │ └── .gitkeep ├── .gitignore ├── resources │ ├── icon.png │ └── deactivate-boundary-inline-elements.css ├── package.json ├── eslint.config.mjs ├── Makefile ├── manifest.json ├── common │ ├── constants.js │ ├── common.js │ └── commonConfigs.js ├── options │ ├── options.css │ ├── init.js │ └── options.html ├── content_scripts │ ├── xpath.js │ ├── content.js │ └── range.js ├── _locales │ ├── ja │ │ └── messages.json │ └── en │ │ └── messages.json └── background │ ├── background.js │ └── uriMatcher.js ├── icon.ai ├── icon.png ├── Makefile ├── .github └── workflows │ └── main.yml ├── COPYING.txt ├── .gitmodules ├── ISSUE_TEMPLATE.md ├── README.md ├── CONTRIBUTING.md ├── history.ja.md ├── history.en.md └── licenses └── MPL2.0.txt /webextensions/extlib/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piroor/textlink/HEAD/icon.ai -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piroor/textlink/HEAD/icon.png -------------------------------------------------------------------------------- /webextensions/.gitignore: -------------------------------------------------------------------------------- 1 | *.xpi 2 | extlib/*.js 3 | extlib/*.dat 4 | 5 | # node.js dependency 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /webextensions/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piroor/textlink/HEAD/webextensions/resources/icon.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE_NAME = textlink 2 | 3 | .PHONY: all xpi install_hook lint format 4 | 5 | all: xpi 6 | 7 | xpi: 8 | cd webextensions && $(MAKE) 9 | cp webextensions/$(PACKAGE_NAME)*.xpi ./ 10 | 11 | install_hook: 12 | echo '#!/bin/sh\nmake lint' > "$(CURDIR)/.git/hooks/pre-commit" && chmod +x "$(CURDIR)/.git/hooks/pre-commit" 13 | 14 | lint: 15 | cd webextensions && $(MAKE) $@ 16 | 17 | format: 18 | cd webextensions && $(MAKE) $@ 19 | 20 | -------------------------------------------------------------------------------- /webextensions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "textlink-we", 3 | "version": "0.0.0", 4 | "engines": { 5 | "node": ">=8.6.0" 6 | }, 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "dependencies": { 11 | "babel-core": "^6.0.0", 12 | "babel-plugin-module-resolver": "^3.0.0", 13 | "eslint": "^9.15.0", 14 | "eslint-import-resolver-babel-module": "^4.0.0", 15 | "eslint-plugin-import": "*", 16 | "jsonlint-cli": "*", 17 | "tunnel-agent": ">=0.6.0" 18 | }, 19 | "devDependencies": { 20 | "globals": "^15.12.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: prepare manifest.json with a revision number 13 | run: | 14 | cp webextensions/manifest.json ./ 15 | version=$(cat manifest.json | jq -r ".version" | sed -r -e "s/$/.$(git log --oneline | wc -l)/") 16 | cat manifest.json | jq ".version |= \"$version\"" > webextensions/manifest.json 17 | - name: build xpi 18 | run: make 19 | - uses: actions/upload-artifact@v4 20 | with: 21 | name: textlink-we.xpi 22 | path: textlink-we.xpi 23 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | Each file under this directory is licensed under MPL 2.0 by default, if the file includes no license information. 2 | All rights reserved. 3 | 4 | ---- 5 | 6 | This project includes files licensed under different type licenses: 7 | 8 | * Mozilla Public License 2.0 (MPL 2.0) 9 | * MIT License 10 | 11 | Please note that these licenses are applied "per-file". In most cases each file includes a license header in itself. If a file has no license header due to some reasons (for example, image files), please see a file named "license.txt" in closest parent directory. Otherwise please treat the file is licensed under the default license declared at the top of this file. 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "makexpi"] 2 | path = makexpi 3 | url = https://github.com/piroor/makexpi.git 4 | [submodule "submodules/webextensions-lib-configs"] 5 | path = submodules/webextensions-lib-configs 6 | url = https://github.com/piroor/webextensions-lib-configs.git 7 | [submodule "submodules/webextensions-lib-l10n"] 8 | path = submodules/webextensions-lib-l10n 9 | url = https://github.com/piroor/webextensions-lib-l10n.git 10 | [submodule "submodules/webextensions-lib-options"] 11 | path = submodules/webextensions-lib-options 12 | url = https://github.com/piroor/webextensions-lib-options.git 13 | [submodule "submodules/publicsuffixlist"] 14 | path = submodules/publicsuffixlist 15 | url = https://github.com/publicsuffix/list.git 16 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Short description 7 | 8 | ## Steps to reproduce 9 | 10 | 1. Start Firefox with clean profile. 11 | 2. Install Text Link. 12 | 3. 13 | 4. 14 | 15 | 19 | 20 | ## Expected result 21 | 22 | 23 | ## Actual result 24 | 25 | 26 | ## Environment 27 | 28 | * Platform (OS): 29 | * Version of Firefox: 30 | * Version (or revision) of Text Link: 31 | -------------------------------------------------------------------------------- /webextensions/resources/deactivate-boundary-inline-elements.css: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /* 8 | An elements with "display:inline-flex" is visually rendered as a regular 9 | inline box, but it is actually treated as a non-inline box internally. 10 | The API "browser.find.find()" (and Firefox's native in-page find feature also) 11 | fails to find a term across the boundary. Thus we need to change its "display" 12 | temporary while finding URI-like texts in webpages. 13 | See also: https://bugzilla.mozilla.org/show_bug.cgi?id=1806291 14 | */ 15 | .textlink-boundary-inline-node:not(#never#match#to#any#element) { 16 | display: inline !important; 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Text Link 2 | 3 | ![Build Status](https://github.com/piroor/textlink/actions/workflows/main.yml/badge.svg?branch=trunk) 4 | 5 | * [Signed package on AMO](https://addons.mozilla.org/firefox/addon/text-link/) 6 | * [Development builds for each commit are available at "Artifacts" of the CI/CD action](https://github.com/piroor/textlink/actions?query=workflow%3ACI%2FCD) 7 | 8 | ## Privacy Policy 9 | 10 | This software does not collect any privacy data automatically, but this includes ability to synchronize options across multiple devices automatically via Firefox Sync. 11 | Any data you input to options may be sent to Mozilla's Sync server, if you configure Firefox to activate Firefox Sync. 12 | 13 | このソフトウェアはいかなるプライバシー情報も自動的に収集しませんが、Firefox Syncを介して自動的に設定情報をデバイス間で同期する機能を含みます。 14 | Firefox Syncを有効化している場合、設定画面に入力されたデータは、Mozillaが運用するSyncサーバーに送信される場合があります。 15 | 16 | -------------------------------------------------------------------------------- /webextensions/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | 3 | export default [{ 4 | ignores: ["eslint.config.mjs", "extlib/*", "!**/.eslintrc.js"], 5 | }, { 6 | languageOptions: { 7 | globals: { 8 | ...globals.browser, 9 | ...globals.webextensions, 10 | }, 11 | 12 | ecmaVersion: 2022, 13 | sourceType: "script", 14 | }, 15 | 16 | settings: { 17 | "import/resolver": { 18 | "babel-module": { 19 | root: ["./"], 20 | }, 21 | }, 22 | }, 23 | 24 | rules: { 25 | "no-const-assign": "error", 26 | 27 | "prefer-const": ["warn", { 28 | destructuring: "any", 29 | ignoreReadBeforeAssign: false, 30 | }], 31 | 32 | indent: ["warn", 2, { 33 | SwitchCase: 1, 34 | MemberExpression: 1, 35 | 36 | CallExpression: { 37 | arguments: "first", 38 | }, 39 | 40 | VariableDeclarator: { 41 | var: 2, 42 | let: 2, 43 | const: 3, 44 | }, 45 | }], 46 | 47 | quotes: ["warn", "single", { 48 | avoidEscape: true, 49 | allowTemplateLiterals: true, 50 | }], 51 | }, 52 | }]; -------------------------------------------------------------------------------- /webextensions/Makefile: -------------------------------------------------------------------------------- 1 | NPM_MOD_DIR := $(CURDIR)/node_modules 2 | NPM_BIN_DIR := $(NPM_MOD_DIR)/.bin 3 | 4 | .PHONY: xpi install_dependency lint format init_extlib update_extlib install_extlib 5 | 6 | all: xpi 7 | 8 | install_dependency: 9 | [ -e "$(NPM_BIN_DIR)/eslint" -a -e "$(NPM_BIN_DIR)/jsonlint-cli" ] || npm install 10 | 11 | lint: install_dependency 12 | "$(NPM_BIN_DIR)/eslint" . --report-unused-disable-directives 13 | find . -type d -name node_modules -prune -o -type f -name '*.json' -print | xargs "$(NPM_BIN_DIR)/jsonlint-cli" 14 | 15 | format: install_dependency 16 | "$(NPM_BIN_DIR)/eslint" . --ext=.js --report-unused-disable-directives --fix 17 | 18 | xpi: init_extlib install_extlib lint 19 | rm -f ./*.xpi 20 | zip -r -9 textlink-we.xpi manifest.json _locales common content_scripts options background resources extlib -x '*/.*' >/dev/null 2>/dev/null 21 | 22 | init_extlib: 23 | git submodule update --init 24 | 25 | update_extlib: 26 | git submodule foreach 'git checkout trunk || git checkout main || git checkout master && git pull' 27 | 28 | install_extlib: 29 | rm -f extlib/*.js extlib/*.dat 30 | cp ../submodules/webextensions-lib-configs/Configs.js extlib/ 31 | cp ../submodules/webextensions-lib-options/Options.js extlib/ 32 | cp ../submodules/webextensions-lib-l10n/l10n.js extlib/ 33 | cp ../submodules/publicsuffixlist/public_suffix_list.dat extlib/ 34 | 35 | -------------------------------------------------------------------------------- /webextensions/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "version": "6.1.10", 5 | "author": "YUKI \"Piro\" Hiroshi", 6 | "description": "__MSG_extensionDescription__", 7 | "permissions": [ 8 | "clipboardWrite", 9 | "menus", 10 | "find", 11 | "storage" 12 | ], 13 | "icons": { 14 | "32": "resources/icon.png" 15 | }, 16 | "background": { 17 | "scripts": [ 18 | "extlib/Configs.js", 19 | "common/constants.js", 20 | "common/common.js", 21 | "common/commonConfigs.js", 22 | "background/uriMatcher.js", 23 | "background/background.js" 24 | ] 25 | }, 26 | "content_scripts": [ 27 | { 28 | "matches": [""], 29 | "all_frames": true, 30 | "run_at": "document_end", 31 | "js": [ 32 | "common/constants.js", 33 | "common/common.js", 34 | "content_scripts/xpath.js", 35 | "content_scripts/range.js", 36 | "content_scripts/content.js" 37 | ], 38 | "css": [ 39 | "resources/deactivate-boundary-inline-elements.css" 40 | ] 41 | } 42 | ], 43 | "options_ui": { 44 | "page": "options/options.html", 45 | "browser_style": true 46 | }, 47 | "default_locale": "en", 48 | "applications": { 49 | "gecko": { 50 | "id": "{54BB9F3F-07E5-486c-9B39-C7398B99391C}", 51 | "strict_min_version": "64.0" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /webextensions/common/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | var kCOMMAND_TRY_ACTION = 'textlink:try-action'; 9 | var kCOMMAND_FIND_URI_RANGES = 'textlink:find-uri-ranges'; 10 | var kCOMMAND_ACTION_FOR_URIS = 'textlink:action-for-uris'; 11 | var kCOMMAND_FETCH_URI_RANGES = 'textlink:fetch-uri-ranges'; 12 | var kNOTIFY_READY_TO_FIND_URI_RANGES = 'textlink:ready-to-find-uri-ranges'; 13 | 14 | var kNOTIFY_MATCH_ALL_PROGRESS = 'textlink:match-all-progress'; 15 | var kCOMMAND_FETCH_MATCH_ALL_PROGRESS = 'textlink:fetch-match-all-progress'; 16 | 17 | var kACTION_DISABLED = 0; 18 | var kACTION_SELECT = 1 << 1; 19 | var kACTION_OPEN_IN_CURRENT = 1 << 2; 20 | var kACTION_OPEN_IN_WINDOW = 1 << 3; 21 | var kACTION_OPEN_IN_TAB = 1 << 4; 22 | var kACTION_OPEN_IN_BACKGROUND_TAB = 1 << 5; 23 | var kACTION_COPY = 1 << 10; 24 | 25 | var kACTION_NAME_TO_ID = { 26 | 'select': kACTION_SELECT, 27 | 'current': kACTION_OPEN_IN_CURRENT, 28 | 'tab': kACTION_OPEN_IN_TAB, 29 | 'tabBackground': kACTION_OPEN_IN_BACKGROUND_TAB, 30 | 'copy': kACTION_COPY 31 | }; 32 | 33 | var kDOMAIN_MULTIBYTE = 1 << 0; 34 | var kDOMAIN_LAZY = 1 << 1; 35 | var kDOMAIN_IDN = 1 << 2; 36 | 37 | -------------------------------------------------------------------------------- /webextensions/common/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | var gLogContext = '?'; 9 | 10 | function log(message, ...args) 11 | { 12 | if (!window.configs || !configs.debug) 13 | return; 14 | 15 | const nest = (new Error()).stack.split('\n').length; 16 | let indent = ''; 17 | for (let i = 0; i < nest; i++) { 18 | indent += ' '; 19 | } 20 | console.log(`TextLink<${gLogContext}>: ${indent}${message}`, ...args); 21 | } 22 | 23 | async function wait(task = 0, timeout = 0) { 24 | if (typeof task != 'function') { 25 | timeout = task; 26 | task = null; 27 | } 28 | return new Promise((resolve, reject) => { 29 | setTimeout(async () => { 30 | if (task) 31 | await task(); 32 | resolve(); 33 | }, timeout); 34 | }); 35 | } 36 | 37 | function nextFrame() { 38 | return new Promise((resolve, reject) => { 39 | window.requestAnimationFrame(resolve); 40 | }); 41 | } 42 | 43 | const RTL_LANGUAGES = new Set([ 44 | 'ar', 45 | 'he', 46 | 'fa', 47 | 'ur', 48 | 'ps', 49 | 'sd', 50 | 'ckb', 51 | 'prs', 52 | 'rhg', 53 | ]); 54 | 55 | function isRTL() { 56 | const lang = ( 57 | navigator.language || 58 | navigator.userLanguage || 59 | //(new Intl.DateTimeFormat()).resolvedOptions().locale || 60 | '' 61 | ).split('-')[0]; 62 | return RTL_LANGUAGES.has(lang); 63 | } 64 | -------------------------------------------------------------------------------- /webextensions/options/options.css: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | :root > * { 8 | transition: opacity 0.25s ease-out; 9 | } 10 | :root:not(.initialized) > * { 11 | opacity: 0; 12 | } 13 | 14 | :root.rtl { 15 | direction: rtl; 16 | } 17 | 18 | p, ul { 19 | margin: 0 0 0.5em 0; 20 | padding: 0; 21 | } 22 | 23 | ul, 24 | ul li { 25 | list-style: none; 26 | } 27 | 28 | p.sub { 29 | margin-left: 2em; 30 | } 31 | 32 | ul p.sub { 33 | margin-top: 0; 34 | margin-bottom: 0; 35 | } 36 | 37 | .action-definition p, 38 | .action-definition p label { 39 | align-items: center; 40 | display: flex; 41 | flex-direction: row; 42 | } 43 | 44 | .action-definition label:not(:first-child) { 45 | /* 46 | border: 1px solid ThreeDShadow; 47 | border-radius: 2px; 48 | padding: 0.2em; 49 | */ 50 | -moz-appearance: button; 51 | } 52 | 53 | .action-definition label:first-child:not(.checked) ~ label { 54 | opacity: 0.5; 55 | pointer-events: none; 56 | } 57 | 58 | .action-definition label:first-child:not(.checked) ~ label input { 59 | -moz-user-focus: ignore; 60 | } 61 | 62 | .action-definition .delimiter { 63 | margin: 0.2em; 64 | } 65 | 66 | .uri-detection label + span { 67 | align-items: center; 68 | display: flex; 69 | flex-direction: row; 70 | max-width: 90%; 71 | } 72 | 73 | .uri-detection label + span > input:first-child { 74 | flex-grow: 1; 75 | } 76 | 77 | :root:not(.debugging) #debug-configs { 78 | max-height: 0; 79 | overflow: hidden; 80 | } 81 | 82 | :root:not(.debugging) #debug-configs * { 83 | -moz-user-focus: ignore; 84 | -moz-user-input: disabled; 85 | } 86 | -------------------------------------------------------------------------------- /webextensions/content_scripts/xpath.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | // XPath utilities 9 | 10 | function hasClass(className) { 11 | return `contains(concat(" ", normalize-space(@class), " "), " ${className} ")`; 12 | } 13 | 14 | function toLowerCase(target) { 15 | return `translate(${target}, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')`; 16 | } 17 | 18 | var NSResolver = { 19 | lookupNamespaceURI : function(prefix) { 20 | switch (prefix) 21 | { 22 | case 'html': 23 | case 'xhtml': 24 | return 'http://www.w3.org/1999/xhtml'; 25 | case 'xlink': 26 | return 'http://www.w3.org/1999/xlink'; 27 | default: 28 | return ''; 29 | } 30 | } 31 | }; 32 | 33 | function evaluateXPath(expression, context, type) { 34 | if (!type) 35 | type = XPathResult.ORDERED_NODE_SNAPSHOT_TYPE; 36 | let result; 37 | try { 38 | result = (context.ownerDocument || context).evaluate( 39 | expression, 40 | (context || document), 41 | NSResolver, 42 | type, 43 | null 44 | ); 45 | } 46 | catch(e) { 47 | return { 48 | singleNodeValue: null, 49 | snapshotLength: 0, 50 | snapshotItem: function() { 51 | return null 52 | } 53 | }; 54 | } 55 | return result; 56 | } 57 | 58 | function getArrayFromXPathResult(xathResult) { 59 | const max = xathResult.snapshotLength; 60 | const array = new Array(max); 61 | if (!max) 62 | return array; 63 | 64 | for (let i = 0; i < max; i++) { 65 | array[i] = xathResult.snapshotItem(i); 66 | } 67 | return array; 68 | } 69 | -------------------------------------------------------------------------------- /webextensions/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "テキストリンク" }, 3 | "extensionDescription": { "message": "ページ中に書かれたURI文字列をダブルクリックしただけで読み込めるようにします。" }, 4 | 5 | "menu_waiting_label": { "message": "選択範囲内のURIを検出中... ($PROGRESS$%)", 6 | "placeholders": { 7 | "progress": { "content": "$1", "example": "100" } 8 | }}, 9 | "menu_group_single": { "message": "選択範囲中のURI: \"$URI$\"", 10 | "placeholders": { 11 | "count": { "content": "$1", "example": "1" }, 12 | "uri": { "content": "$2", "example": "http://example.net/" } 13 | }}, 14 | "menu_group_multiple": { "message": "選択範囲中の $COUNT$ 個のURI: \"$FIRST_URI$\" ~ \"$LAST_URI$\"", 15 | "placeholders": { 16 | "count": { "content": "$1", "example": "1" }, 17 | "first_uri": { "content": "$2", "example": "http://example.com/" }, 18 | "last_uri": { "content": "$3", "example": "http://example.net/" } 19 | }}, 20 | "menu_direct_single": { "message": "$TITLE$: \"$URI$\"", 21 | "placeholders": { 22 | "title": { "content": "$1", "example": "Open" }, 23 | "uri": { "content": "$2", "example": "http://example.net/" } 24 | }}, 25 | "menu_direct_multiple": { "message": "$TITLE$: \"$FIRST_URI$\"~\"$LAST_URI$\"", 26 | "placeholders": { 27 | "title": { "content": "$1", "example": "Open" }, 28 | "first_uri": { "content": "$2", "example": "http://example.com/" }, 29 | "last_uri": { "content": "$3", "example": "http://example.net/" } 30 | }}, 31 | "menu_openCurrent_single": { "message": "開く" }, 32 | "menu_openCurrent_multiple": { "message": "すべて開く" }, 33 | "menu_openTab_single": { "message": "新しいタブで開く" }, 34 | "menu_openTab_multiple": { "message": "すべて新しいタブで開く" }, 35 | "menu_openWindow_single": { "message": "新しいウィンドウで開く" }, 36 | "menu_openWindow_multiple": { "message": "すべて新しいウィンドウで開く" }, 37 | "menu_copy_single": { "message": "コピー" }, 38 | "menu_copy_multiple": { "message": "すべてコピー" }, 39 | "shortURI": { "message": "$URI$...", 40 | "placeholders": { 41 | "uri": { "content": "$1", "example": "http://example.com/" } 42 | }}, 43 | 44 | 45 | "config_action_caption": { "message": "URI文字列上での操作" }, 46 | 47 | "config_action_group_action": { "message": "通常の場面" }, 48 | "config_action_group_actionInEditable": { "message": "入力フィールド内" }, 49 | 50 | "config_action_select": { "message": "URI文字列を選択する" }, 51 | "config_action_current": { "message": "現在のタブに読み込む" }, 52 | "config_action_window": { "message": "新しいウィンドウで開く" }, 53 | "config_action_tab": { "message": "新しいアクティブなタブで開く" }, 54 | "config_action_tabBackground": { "message": "新しいタブで開く" }, 55 | "config_action_copy": { "message": "クリップボードにコピーする" }, 56 | 57 | "config_trigger_dblclick": { "message": "ダブルクリック" }, 58 | "config_trigger_enter": { "message": "Enterキー" }, 59 | "config_trigger_alt": { "message": "Alt" }, 60 | "config_trigger_ctrl": { "message": "Ctrl/Control" }, 61 | "config_trigger_meta": { "message": "Meta/⌘" }, 62 | "config_trigger_shift": { "message": "Shift" }, 63 | 64 | 65 | "config_menu_caption": { "message": "コンテキストメニューの項目" }, 66 | "menu_single_description": { "message": "(URIが1つだけの場合)" }, 67 | "menu_multiple_description": { "message": "(URIが複数ある場合)" }, 68 | 69 | "config_advanced_caption": { "message": "詳細" }, 70 | "config_showProgress": { "message": "選択範囲中のURIの検出処理の進行状況を表示する" }, 71 | "config_detection_group": { "message": "URIの検出方法" }, 72 | "config_scheme": { "message": "以下で始まる文字列をURIとして認識する(例:「http,ftp,mailto」): " }, 73 | "config_scheme_default": { "message": "初期値に戻す" }, 74 | "config_scheme_fixup_table": { "message": "以下のルールに従ってURIを補完する(例:「ttp=>http,ttps=>https」)" }, 75 | "config_scheme_fixup_table_default": { "message": "初期値に戻す" }, 76 | "config_relative_enabled": { "message": "相対パスも認識する(非推奨)" }, 77 | "config_multibyte_enabled": { "message": "全角英数文字も認識する" }, 78 | "config_multiline_enabled": { "message": "改行されたURIも認識する(非推奨)" }, 79 | "config_idn_enabled": { "message": "以下で始まるURIでは国際化ドメイン(IDN)も認識する" }, 80 | "config_idn_scheme_default": { "message": "初期値に戻す" }, 81 | "config_i18nPath_enabled": { "message": "非ASCII文字のパスも認識する" }, 82 | 83 | 84 | "config_debug_caption": { "message": "開発用" }, 85 | "config_debug_label": { "message": "デバッグモード" }, 86 | "config_all_caption": { "message": "すべての設定項目" } 87 | } 88 | -------------------------------------------------------------------------------- /webextensions/options/init.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | gLogContext = 'Options'; 9 | let options; 10 | 11 | function onConfigChanged(key) { 12 | switch (key) { 13 | case 'debug': 14 | if (configs.debug) 15 | document.documentElement.classList.add('debugging'); 16 | else 17 | document.documentElement.classList.remove('debugging'); 18 | break; 19 | } 20 | 21 | const checkbox = document.querySelector(`label > input[type="checkbox"]#${key}`); 22 | if (checkbox) { 23 | if (checkbox.checked) 24 | checkbox.parentNode.classList.add('checked'); 25 | else 26 | checkbox.parentNode.classList.remove('checked'); 27 | } 28 | } 29 | 30 | function actionGroup(params) { 31 | return ` 32 |

__MSG_config_action_group_${params.group}__

33 | ${params.content} 34 | `; 35 | } 36 | 37 | function actionFieldSet(params) { 38 | return ` 39 |
40 | __MSG_config_action_${params.action}__ 41 | ${params.content} 42 |
43 | `; 44 | } 45 | 46 | function actionCheckboxes(params) { 47 | const base = params.base; 48 | const action = params.action; 49 | const type = params.type; 50 | return ` 51 |

54 | - 55 | 58 | - 59 | 62 | - 63 | 66 | - 67 |

70 | `; 71 | } 72 | 73 | window.addEventListener('DOMContentLoaded', async () => { 74 | document.documentElement.classList.toggle('rtl', isRTL()); 75 | await configs.$loaded; 76 | configs.$addObserver(onConfigChanged); 77 | 78 | const fragment = document.createDocumentFragment(); 79 | const range = document.createRange(); 80 | range.selectNodeContents(document.querySelector('#actions')); 81 | range.insertNode(range.createContextualFragment( 82 | ['action', 'actionInEditable'].map(base => 83 | actionGroup({ 84 | group: base, 85 | content: ['select', 'current', 'tab', 'tabBackground', 'window', 'copy'].map(action => 86 | actionFieldSet({ 87 | action, 88 | content: ['dblclick', 'enter'].map(type => 89 | actionCheckboxes({ base, action, type })).join('\n') 90 | })).join('\n') 91 | })).join('\n') 92 | )); 93 | range.detach(); 94 | l10n.updateDocument(); 95 | 96 | options = new Options(configs); 97 | options.onReady(); 98 | options.buildUIForAllConfigs(document.querySelector('#debug-configs')); 99 | onConfigChanged('debug'); 100 | 101 | setTimeout(() => { 102 | for (const checkbox of document.querySelectorAll('label > input[type="checkbox"]')) { 103 | if (checkbox.checked) 104 | checkbox.parentNode.classList.add('checked'); 105 | else 106 | checkbox.parentNode.classList.remove('checked'); 107 | } 108 | }, 0); 109 | 110 | for (const resetButton of document.querySelectorAll('[data-reset-target]')) { 111 | const id = resetButton.getAttribute('data-reset-target'); 112 | const field = document.querySelector(`#${id}`); 113 | if (!field) 114 | continue; 115 | resetButton.addEventListener('click', () => { 116 | field.$reset(); 117 | }); 118 | resetButton.addEventListener('keyup', (event) => { 119 | if (event.key == 'Enter') 120 | field.$reset(); 121 | }); 122 | } 123 | 124 | document.documentElement.classList.add('initialized'); 125 | }, { once: true }); 126 | -------------------------------------------------------------------------------- /webextensions/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

__MSG_config_action_caption__

19 |
20 | 21 |

__MSG_config_menu_caption__

22 |

26 |

30 |

34 |

38 |

42 |

46 |

50 |

54 | 55 |

__MSG_config_advanced_caption__

56 |

59 |
60 | __MSG_config_detection_group__ 61 |

62 | 64 |

65 |

68 | 70 |

71 |

72 | 74 |

75 |

78 |

81 |

84 |
85 | 86 |
87 | 88 |

__MSG_config_debug_caption__

89 |

92 |
93 |

__MSG_config_all_caption__

94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /webextensions/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { "message": "Text Link" }, 3 | "extensionDescription": { "message": "Allows URI texts written in webpages to be loaded by double clicks." }, 4 | 5 | "menu_waiting_label": { "message": "Finding URIs in Selection… ($PROGRESS$%)", 6 | "placeholders": { 7 | "progress": { "content": "$1", "example": "100" } 8 | }}, 9 | "menu_group_single": { "message": "URI in Selection: \"$URI$\"", 10 | "placeholders": { 11 | "count": { "content": "$1", "example": "1" }, 12 | "uri": { "content": "$2", "example": "http://example.net/" } 13 | }}, 14 | "menu_group_multiple": { "message": "$COUNT$ URIs in Selection: \"$FIRST_URI$\" ~ \"$LAST_URI$\"", 15 | "placeholders": { 16 | "count": { "content": "$1", "example": "1" }, 17 | "first_uri": { "content": "$2", "example": "http://example.com/" }, 18 | "last_uri": { "content": "$3", "example": "http://example.net/" } 19 | }}, 20 | "menu_direct_single": { "message": "$TITLE$: \"$URI$\"", 21 | "placeholders": { 22 | "title": { "content": "$1", "example": "Open" }, 23 | "uri": { "content": "$3", "example": "http://example.net/" } 24 | }}, 25 | "menu_direct_multiple": { "message": "$TITLE$: \"$FIRST_URI$\" ~ \"$LAST_URI$\"", 26 | "placeholders": { 27 | "title": { "content": "$1", "example": "Open" }, 28 | "first_uri": { "content": "$2", "example": "http://example.com/" }, 29 | "last_uri": { "content": "$3", "example": "http://example.net/" } 30 | }}, 31 | "menu_openCurrent_single": { "message": "Open" }, 32 | "menu_openCurrent_multiple": { "message": "Open All" }, 33 | "menu_openTab_single": { "message": "Open in New Tab" }, 34 | "menu_openTab_multiple": { "message": "Open All in New Tabs" }, 35 | "menu_openWindow_single": { "message": "Open in New Window" }, 36 | "menu_openWindow_multiple": { "message": "Open All in New Windows" }, 37 | "menu_copy_single": { "message": "Copy" }, 38 | "menu_copy_multiple": { "message": "Copy All" }, 39 | "shortURI": { "message": "$URI$…", 40 | "placeholders": { 41 | "uri": { "content": "$1", "example": "http://example.com/" } 42 | }}, 43 | 44 | 45 | "config_action_caption": { "message": "Actions for URI text" }, 46 | 47 | "config_action_group_action": { "message": "in Regular Context" }, 48 | "config_action_group_actionInEditable": { "message": "in Input Field" }, 49 | 50 | "config_action_select": { "message": "Just Select" }, 51 | "config_action_current": { "message": "Load in the Current Tab" }, 52 | "config_action_window": { "message": "Open in a New Window" }, 53 | "config_action_tab": { "message": "Open in a New Active Tab" }, 54 | "config_action_tabBackground": { "message": "Open in a New Tab" }, 55 | "config_action_copy": { "message": "Copy to the Clipboard" }, 56 | 57 | "config_trigger_dblclick": { "message": "Double-click" }, 58 | "config_trigger_enter": { "message": "Enter key" }, 59 | "config_trigger_alt": { "message": "Alt" }, 60 | "config_trigger_ctrl": { "message": "Ctrl/Control" }, 61 | "config_trigger_meta": { "message": "Meta/⌘" }, 62 | "config_trigger_shift": { "message": "Shift" }, 63 | 64 | 65 | "config_menu_caption": { "message": "Context Menu Items" }, 66 | "menu_single_description": { "message": " (for single URI)" }, 67 | "menu_multiple_description": { "message": " (for multiple URIs)" }, 68 | 69 | "config_advanced_caption": { "message": "Advanced" }, 70 | "config_showProgress": { "message": "Show progress of finding URIs in selection" }, 71 | "config_detection_group": { "message": "URI Detection" }, 72 | "config_scheme": { "message": "Recognize text which starts with following parts, as URI: (ex. \"http,ftp,mailto\")" }, 73 | "config_scheme_default": { "message": "Reset" }, 74 | "config_scheme_fixup_table": { "message": "Fix up broken URI, with following rules: (ex. \"ttp=>http,ttps=>https\")" }, 75 | "config_scheme_fixup_table_default": { "message": "Reset" }, 76 | "config_relative_enabled": { "message": "Parse relative paths (unrecommended)" }, 77 | "config_multibyte_enabled": { "message": "Parse multi-byte characters" }, 78 | "config_multiline_enabled": { "message": "Parse URIs split in multiple lines (unrecommended)" }, 79 | "config_idn_enabled": { "message": "Parse IDN(Internationalized Domain Names)s for URIs starts with" }, 80 | "config_idn_scheme_default": { "message": "Reset" }, 81 | "config_i18nPath_enabled": { "message": "Parse non-ASCII paths" }, 82 | 83 | 84 | "config_debug_caption": { "message": "Development" }, 85 | "config_debug_label": { "message": "Debug mode" }, 86 | "config_all_caption": { "message": "All configuration items" } 87 | } 88 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guideline 2 | 3 | If you are planning to open a new issue for a bug report or a feature request, or having additional information for an existing issue, or hoping to translate the language resource for your language, then please see this document before posting. 4 | 5 | This is possibly a generic guideline for contributing any Firefox addon project or a public OSS/free software project. 6 | 7 | ## Good, helpful bug reports including feature requests 8 | 9 | A good report is the fastest way to solve a problem. 10 | Even if the problem is very clear for you, possibly unclear for me. 11 | Unclear report can be left unfixed for long time. 12 | You'll see an example of [good report](https://github.com/piroor/treestyletab/issues/1134) and [another report with too less information](https://github.com/piroor/treestyletab/issues/1135). 13 | 14 | Here is a list of typical questions I asked to existing reports: 15 | 16 | * **Does the problem appear with the [latest develpment build](http://piro.sakura.ne.jp/xul/xpi/nightly/)?** 17 | Possibly, problems you met has been resolved already. 18 | On Firefox 48 and later, you'll have to use an [unbranded Firefox](https://wiki.mozilla.org/Add-ons/Extension_Signing#Unbranded_Builds) (including Beta, Aurora, and Nightly) with a secret preference `xpinstall.signatures.required`=`false` (you can set it via `about:config`), to try unsigned development builds. 19 | * **Is the "problem" really introduced by this addon?** 20 | If you use this addon with others, please confirm which addon actually causes the problem you met. 21 | If the problem doesn't appear with no other addon, it can be introduced by others. 22 | See also the next. 23 | * **Does the problem appear with a clean profile?** 24 | You can start Firefox with temporary clean profile by a command line `-profile`, like: `"C:\Program Filex (x86)\Mozilla Firefox\firefox.exe" -no-remote -profile "%Temp%\FirefoxTemporaryProfile"` 25 | If the problem doesn't appear with a clean profile, please find complete reproduction steps out. 26 | * **Is the main topic single and clear?** 27 | Sometimes I got an issue including multiple topics, but such an issue is hard to be closed, then it often stays opened for long time and confuses me. 28 | If you have multiple topics, please report them as separate issues for each. 29 | 30 | For future requests, some more important information: 31 | 32 | * **Did you find other addon which provide the feature you are going to request?** 33 | If there is any other addon for the purpose, then this addon should become compatible to it instead of merging the feature into this addon itself. 34 | * **Please add `[feature request]` tag into the summary.** 35 | Sometimes a feature request can be misunderstood as a simple bug report. 36 | 37 | Then, please report the bug with these information: 38 | 39 | * **Detailed steps to reproduce the problem.** For example: 40 | 1. Prepare Firefox version XX with plain profile. 41 | 2. Install Tree Style Tab version XXXX. 42 | 3. Install another addon XXXX version XXXX from "http://....". 43 | 4. Click a button on the toolbar. 44 | 5. ... 45 | * **Expected result.** 46 | If you have any screenshot or screencast, it will help me more. 47 | * **Actual result.** 48 | If you have any screenshot or screencast, it will help me more. 49 | * **Platform information.** 50 | If the problem appear on your multiple platforms, please list them. 51 | 52 | If your issue is related to something complex conditions, figures or screenshots will help me a lot, instead of long descriptions. 53 | 54 | ## Please don't join to an existing discussion if your problem is different from the originally reported one 55 | 56 | Even if the result is quite similar, they may be different problem if the reproduction steps for yours are different from the one originally reported. 57 | Then, you should create a new issue for yours, instead of adding comment to the existing issue. 58 | Otherwise, I'll be confused if the original reporter said "the issue is a compatibility issue with another addon" but another reporter said "I saw this problem without any other addon". 59 | 60 | To avoid such a confusion, please post your report with detailed reproduction steps always. 61 | 62 | ## Feature requests can be tagged as "out of purpose" 63 | 64 | If there is any [readme page](./README.md), please see it before you post a new feature request. 65 | (Some my addon doesn't provide such an information, sorry.) 66 | Even if a requested feature is very useful, it is possibly rejected by the project policy. 67 | 68 | Instead, please tell me other addon which provide the feature and report a new issue as "compatibility issue with the addon, this addon should work together with it". 69 | I'm very positive to make my addons compatible to others. 70 | 71 | ## Translations, pull requests 72 | 73 | If you've fixed a problem you met by your hand, then please send a pull request to me. 74 | 75 | Translations also. 76 | Pull requests are easy to merge, than sending ZIP files. 77 | You'll do it without any local application - you can do it on the GitHub. 78 | For example, if you want to fix an existing typo in a locale, you just have to click the pencil button (with a tooltip "Edit this file") for a language resource file. 79 | -------------------------------------------------------------------------------- /webextensions/common/commonConfigs.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | const defaultActions = [ 9 | { action: 'select', 10 | triggerMouse: '', 11 | triggerKey: '' }, 12 | { action: 'current', 13 | triggerMouse: 'accel,dblclick', 14 | triggerKey: 'accel,enter' }, 15 | { action: 'tab', 16 | triggerMouse: 'dblclick', 17 | triggerKey: 'enter' }, 18 | { action: 'tabBackground', 19 | triggerMouse: 'shift,dblclick', 20 | triggerKey: 'shift,enter' }, 21 | { action: 'window', 22 | triggerMouse: '', 23 | triggerKey: '' }, 24 | { action: 'copy', 25 | triggerMouse: '', 26 | triggerKey: '' } 27 | ]; 28 | const defaultActionsInEditable = [ 29 | { action: 'select', 30 | triggerMouse: 'dblclick', 31 | triggerKey: '' }, 32 | { action: 'current', 33 | triggerMouse: '', 34 | triggerKey: '' }, 35 | { action: 'tab', 36 | triggerMouse: 'accel,dblclick', 37 | triggerKey: 'accel,enter' }, 38 | { action: 'tabBackground', 39 | triggerMouse: '', 40 | triggerKey: '' }, 41 | { action: 'window', 42 | triggerMouse: '', 43 | triggerKey: '' }, 44 | { action: 'copy', 45 | triggerMouse: '', 46 | triggerKey: '' } 47 | ]; 48 | 49 | const defaultConfigs = { 50 | menu_openCurrent_single: false, 51 | menu_openCurrent_multiple: false, 52 | menu_openTab_single: true, 53 | menu_openTab_multiple: true, 54 | menu_openWindow_single: true, 55 | menu_openWindow_multiple: false, 56 | menu_copy_single: true, 57 | menu_copy_multiple: true, 58 | 59 | showProgress: true, 60 | scheme: 'http https ftp news nntp telnet irc mms ed2k about file urn chrome resource data', 61 | schemeFixupTable: 'www=>http://www ftp.=>ftp://ftp. irc.=>irc:irc. h??p=>http h???s=>https ttp=>http tp=>http p=>http ttps=>https tps=>https ps=>https', 62 | schemeFixupDefault: 'http', 63 | relativeEnabled: false, 64 | multibyteEnabled: true, 65 | multilineEnabled: false, 66 | IDNEnabled: true, 67 | IDNScheme: 'http https ftp news nntp telnet irc', 68 | i18nPathEnabled: false, 69 | partExceptionWhole: '-+|=+|(?:-=)+-?|(?:=-)=?|\\#+|\\++|\\*+|~+|[+-]?\\d+:\\d+(?::\\d+)?', 70 | partExceptionStart: '-+|=+|(?:-=)+-?|(?:=-)=?|\\#+|\\++|\\*+|~+|[+-]?\\d+:\\d+(?::\\d+)?|[\\.\u3002\uff0e]+[^\\.\u3002\uff0e\/\uff0f]', 71 | partExceptionEnd: '-+|=+|(?:-=)+-?|(?:=-)=?|\\#+|\\++|\\*+|~+|[+-]?\\d+:\\d+(?::\\d+)?', 72 | IDNLazyDetectionSeparators: '\u3001\u3002', 73 | 74 | rangeFindTimeout: 500, 75 | rangeFindRetryDelay: 100, 76 | 77 | // Services.prefs.getStringPref('network.IDN.blacklist_chars').split('').map(aChar => `\\u${('0000'+aChar.charCodeAt(0).toString(16)).substr(-4)}`).join('') 78 | IDNBlacklistChars: '\u0020\u00a0\u00bc\u00bd\u00be\u01c3\u02d0\u0337\u0338\u0589\u058a\u05c3\u05f4\u0609\u060a\u066a\u06d4\u0701\u0702\u0703\u0704\u115f\u1160\u1735\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200e\u200f\u2010\u2019\u2024\u2027\u2028\u2029\u202a\u202b\u202c\u202d\u202e\u202f\u2039\u203a\u2041\u2044\u2052\u205f\u2153\u2154\u2155\u2156\u2157\u2158\u2159\u215a\u215b\u215c\u215d\u215e\u215f\u2215\u2236\u23ae\u2571\u29f6\u29f8\u2afb\u2afd\u2ff0\u2ff1\u2ff2\u2ff3\u2ff4\u2ff5\u2ff6\u2ff7\u2ff8\u2ff9\u2ffa\u2ffb\u3000\u3002\u3014\u3015\u3033\u30a0\u3164\u321d\u321e\u33ae\u33af\u33c6\u33df\ua789\ufe14\ufe15\ufe3f\ufe5d\ufe5e\ufeff\uff0e\uff0f\uff61\uffa0\ufff9\ufffa\ufffb\ufffc\ufffd', 79 | 80 | debug: false 81 | }; 82 | 83 | { 84 | const isMac = /^Mac/i.test(navigator.platform); 85 | for (const action of defaultActions) { 86 | defaultConfigs[`action_${action.action}_dblclick`] = /dblclick/.test(action.triggerMouse); 87 | defaultConfigs[`action_${action.action}_dblclick_alt`] = /alt/.test(action.triggerMouse); 88 | defaultConfigs[`action_${action.action}_dblclick_ctrl`] = /ctrl/.test(action.triggerMouse) || !isMac && /accel/.test(action.triggerMouse); 89 | defaultConfigs[`action_${action.action}_dblclick_meta`] = /meta/.test(action.triggerMouse) || isMac && /accel/.test(action.triggerMouse); 90 | defaultConfigs[`action_${action.action}_dblclick_shift`] = /shift/.test(action.triggerMouse); 91 | defaultConfigs[`action_${action.action}_enter`] = /enter/.test(action.triggerKey); 92 | defaultConfigs[`action_${action.action}_enter_alt`] = /alt/.test(action.triggerKey); 93 | defaultConfigs[`action_${action.action}_enter_ctrl`] = /ctrl/.test(action.triggerKey) || !isMac && /accel/.test(action.triggerKey); 94 | defaultConfigs[`action_${action.action}_enter_meta`] = /meta/.test(action.triggerKey) || isMac && /accel/.test(action.triggerKey); 95 | defaultConfigs[`action_${action.action}_enter_shift`] = /shift/.test(action.triggerKey); 96 | } 97 | for (const action of defaultActionsInEditable) { 98 | defaultConfigs[`actionInEditable_${action.action}_dblclick`] = /dblclick/.test(action.triggerMouse); 99 | defaultConfigs[`actionInEditable_${action.action}_dblclick_alt`] = /alt/.test(action.triggerMouse); 100 | defaultConfigs[`actionInEditable_${action.action}_dblclick_ctrl`] = /ctrl/.test(action.triggerMouse) || !isMac && /accel/.test(action.triggerMouse); 101 | defaultConfigs[`actionInEditable_${action.action}_dblclick_meta`] = /meta/.test(action.triggerMouse) || isMac && /accel/.test(action.triggerMouse); 102 | defaultConfigs[`actionInEditable_${action.action}_dblclick_shift`] = /shift/.test(action.triggerMouse); 103 | defaultConfigs[`actionInEditable_${action.action}_enter`] = /enter/.test(action.triggerKey); 104 | defaultConfigs[`actionInEditable_${action.action}_enter_alt`] = /alt/.test(action.triggerKey); 105 | defaultConfigs[`actionInEditable_${action.action}_enter_ctrl`] = /ctrl/.test(action.triggerKey) || !isMac && /accel/.test(action.triggerKey); 106 | defaultConfigs[`actionInEditable_${action.action}_enter_meta`] = /meta/.test(action.triggerKey) || isMac && /accel/.test(action.triggerKey); 107 | defaultConfigs[`actionInEditable_${action.action}_enter_shift`] = /shift/.test(action.triggerKey); 108 | } 109 | } 110 | 111 | var configs = new Configs(defaultConfigs, { 112 | syncKeys: Object.keys(defaultConfigs) 113 | }); 114 | -------------------------------------------------------------------------------- /history.ja.md: -------------------------------------------------------------------------------- 1 | # 更新履歴 2 | 3 | - master/HEAD 4 | - 6.1.10 (2025.5.27) 5 | * 選択範囲内の複数のURI文字列を正しく列挙するよう修正(6.1.5での後退バグ) 6 | * 部分選択からURIを検出する際に、狙った位置よりも前により短いURIがあるとそちらが検出されてしまっていたのを修正 7 | - 6.1.9 (2023.3.25) 8 | * 文字列選択時の動作を高速化(視覚的にインラインになっていないコンテナーを検知して、可能な限り早期に打ち切るようにした) 9 | - 6.1.8 (2023.3.24) 10 | * 文字列選択時の動作を高速化(選択範囲の前後のテキストの探索を、ブロックレベルのコンテナーの切り替わりを検知して、可能な限り早期に打ち切るようにした) 11 | - 6.1.7 (2023.1.7) 12 | * 事実上のインライン要素で区切られたURL文字列を受け付けるように↓(例:Twitter上でのスクリーンネームのリンクを含むURL文字列) 13 | * 要素ノードの可視性を確認する処理を高速化 14 | * 有効なTLDのリストを更新([public suffix list](https://publicsuffix.org/)を使用) 15 | - 6.1.6 (2021.12.21) 16 | * キー操作に対する反応をkeyupではなくkeydownのタイミングで行うようにした(この変更により、Enterの直後にURLを含む文字列をCtrl-Vで貼り付けた場合のCtrl-Enterとの誤認を防ぎます) 17 | * 有効なTLDのリストを更新([public suffix list](https://publicsuffix.org/)を使用) 18 | - 6.1.5 (2021.11.5) 19 | * 長いURI文字列が選択されているときに、その選択範囲よりも前に、そのURI文字列の一部と一致するURIがある場合の、部分的なURIと長いURIの両方が選択範囲から見つかったURIとして列挙されてしまう問題を修正 20 | - 6.1.4 (2021.11.4) 21 | * コンテキストメニューからの操作において選択範囲からのURIの検出に失敗する場合があったのを修正 22 | - 6.1.3 (2021.10.25) 23 | * コンテキストメニューの項目が機能していなかったのを修正(by [gontazaka](https://github.com/gontazaka), thanks!) 24 | - 6.1.2 (2020.5.5) 25 | * 既知のトップレベルドメインの情報ソースとして[public suffix list](https://publicsuffix.org/)を使うようにした(URLらしき文字列について、ホスト名部分のトップレベルドメインがこのリストに記載されている物であればURLとして識別するようにした) 26 | * 「すべての設定」のUIの不備を改善:インポートした設定をUIに即座に反映し、また、数値型の一部の設定項目で小数が不正な値として警告されてしまわないようにした 27 | - 6.1.1 (2020.6.8) 28 | * テキスト入力欄でのキー入力操作が意図せず激遅になっていたのを修正 29 | - 6.1.0 (2020.3.10) 30 | * 選択範囲の上でのコンテキストメニューコマンドのラベルが見えなくなっていたのを修正 31 | * Firefoxの最近のバージョンで設定画面が機能しなくなっていたのを修正 32 | * Firefox 63以前のサポートを終了 33 | - 6.0.3 (2019.8.8) 34 | * Firefox 70で廃止される古いコードを削除 35 | * キーボードショートカットを除く全設定のインポートとエクスポートに対応(設定→開発用→デバッグモード→すべての設定→Import/Export) 36 | - 6.0.2 (2018.7.30) 37 | * URIを選択する処理を最適化(従来版では、Webページ内に大量のノードがある場合に処理に時間がかかりすぎる場合があった) 38 | * クリックされたURIの処理中にWebページの内容が改変された場合でも、そのURIを開けるようにした 39 | * 設定をFirefox Syncで同期するようにした 40 | - 6.0.1 (2017.11.4) 41 | * 選択範囲からURI文字列を検出する処理を高速化(必要が生じるまでは実際のRangeを検索しないようにした) 42 | - 6.0.0 (2017.11.3) 43 | * WebExtensionsベースで作り直した 44 | * Thunderbirdへの対応を終了 45 | - 5.0.2016031501 46 | * Nightly 48.0a1対応 47 | * 設定を専用の名前空間の下で管理するようにした 48 | * 「〜をタブで開く」が即座に実行された場合でもすべての処理対象URIを正しく開くように修正 49 | * plロケール更新 ([by Piotr Drąg, thanks!](https://github.com/piroor/textlink/pull/52)) 50 | * 選択範囲にURIが存在しない場合に、コンテキストメニューの項目を正しく無効化するように修正 51 | * コンテキストメニュー内の無効化されたメニュー項目に対してツールチップを表示しないようにした 52 | - 5.0.2015060501 53 | * マルチプロセスモード(E10S)に対応 54 | * hy-AM(アルメニア語)ロケールを追加(by [Hrant Ohanyan](http://haysoft.org). Thanks!) 55 | - 4.1.2013040601 56 | * 選択範囲を自動的に広げる処理が期待通りに動かない場合があったのを修正 57 | * jarファイルを含めない形のパッケージングに変更 58 | - 4.1.2012122901 59 | * Nightly 20.0a1に対応 60 | * Firefox 9およびそれ以前のバージョンへの対応を終了 61 | * コンテキストメニューを開いた時に、選択範囲の中に含まれるURIを少しずつ検索するようにした(これにより、メニューを開くときにフリーズしてしまうということがなくなった) 62 | * ポート番号を含むURIもURIとして認識するようにした 63 | * 1つのURI文字列だけを選択していたときに、正しく現在のタブに読み込むようにした 64 | * いくつかのエッジケースで選択範囲からのURIの認識に失敗していたのを修正 65 | - 4.0.2011021601 66 | * ユーザが設定を変更していた場合に設定の移行に失敗していたのを修正 67 | - 4.0.2011021301 68 | * IDNの解釈を有効にするスキームを明示的に指定できるようにした 69 | * 初期状態でdata: URIを検出するようにした 70 | * 「ユーザ名:パスワード@ドメイン名」形式のURLを受け付けなくなっていたのを修正 71 | * about: URIやchrome: URLなどの、妥当なTLDを含まないURIを検出できなくなっていたのを修正 72 | * テキスト入力欄の中でコンテキストメニューの項目が表示されなくなっていたのを修正 73 | * テキスト入力欄を含む選択範囲からのURI文字列の検索に失敗していたのを修正 74 | - 4.0.2011020501 75 | * Minefieldで選択範囲のURIをまとめてタブで開く機能が動作しなくなっていたのを修正 76 | - 4.0.2011012101 77 | * Minefield 4.0b10pre対応 78 | * Firefox 3.0以前への対応を終了 79 | * Thunderbird 3.1対応 80 | * Thunderbird 2以前への対応を終了 81 | * IDN(国際化ドメイン)を認識できるようにした 82 | * ページ内で何も選択していない時は、コンテキストメニューを開く時に何も処理を行わないようにした 83 | * ドイツ語ロケール追加(by Michael Baer) 84 | - 3.1.2009110201 85 | * 内部に保持しているTLDのリストを更新 86 | * Enterキー以外のキー入力を無視するようにした 87 | * より安全なコードに修正 88 | * フランス語ロケール更新 (by menet) 89 | * トルコ語ロケール更新 (by Anıl Yıldız) 90 | - 3.1.2009032701 91 | * URI文字列の後の開き括弧がURIの一部として認識されてしまっていたのを修正 92 | * ThunderbirdがURIであると誤判定した箇所を通常のテキストに戻す処理について、見落としてしまうケースがいくつかあったのを修正 93 | - 3.1.2009032601 94 | * Thunderbirdでも利用できるようにした 95 | * 「URL:http://……」と言った形で書かれたURIを正しく認識できない問題を修正 96 | * zh-TWロケール更新(by Alan CHENG) 97 | - 3.0.2009031801 98 | * 処理を高速化 99 | - 3.0.2009031701 100 | * Firefox 3で処理を高速化 101 | * ツールチップが消えた後もツールチップの内容の更新が続いていたのを、処理を停止するようにした 102 | * 連続するインライン要素以外はテキストの切れ目として認識するようにした 103 | - 3.0.2009030901 104 | * Firefox 2で正しく動作しなくなっていたのを修正 105 | * プレーンテキストでクリック位置からURIを検出できなくなっていたのを修正 106 | * コンテキストメニューの初期化にかかる時間を短縮した(正確さを犠牲にして処理速度を向上した) 107 | - 3.0.2009022402 108 | * ページの長さに比例して処理速度が大幅に低下してしまっていたのを修正 109 | * 選択範囲外のURI文字列がヒットする可能性があったのを修正 110 | - 3.0.2009022401 111 | * テキストファイルでクリック位置の前後のテキストの検出に失敗していたのを修正(単一のテキストノードの場合、検索範囲がテキストノードの前に設定されていなかった) 112 | * 非表示のテキストを含むページで、選択した位置のURIを正しく認識できない場合があったのを修正 113 | * 全角文字で書かれたURIについて、\u301c(波ダッシュ)と\uff5e(全角チルダ)の両方とも「~」として受け付けるようにした 114 | - 3.0.2009021901 115 | * www.やftp.で始まるドメイン名の補完ルールの初期設定を修正 116 | * トルコ語ロケール更新 (by Anıl Yıldız) 117 | - 3.0.2009021801 118 | * テキストノードが細かく分割されている場所で処理に時間がかかっていたのを修正 119 | - 3.0.2009021601 120 | * 選択範囲のURIを開く機能をテキスト入力欄の中でも使えるようにした 121 | * GMailのメール編集画面でコンテキストメニューを開こうとするとフリーズする問題を修正(URI文字列の検索の際に、head要素、script要素などの内容は除外するようにした) 122 | - 3.0.2009021502 123 | * URI文字列の後に強制改行と英数字文字列が連続している時に、改行以後の英数字文字列がURIの一部として認識されてしまっていたのを修正 124 | * 相対パスの解釈が無効な時は、ドメイン名などの前に書かれた文字のうち、URI文字列の先頭に登場し得ない文字を無視するようにした 125 | * フランス語ロケール更新 (by menet) 126 | - 3.0.2009021401 127 | * 選択範囲のURI文字列をまとめてクリップボードにコピーする機能を追加 128 | * クリックした位置のURI文字列をクリップボードにコピーする機能を追加 129 | * Firefox 3以降の複数に分割された選択範囲に対応 130 | * コンテキストメニューの項目の上で、選択されている全てのURIをツールチップで表示するようにした 131 | * クリック位置のURI文字列を選択する処理について、前後の括弧などを除外するようにした 132 | - 2.1.2009021301 133 | * Firefox 2より前のバージョンへの対応を終了 134 | * 設定ダイアログをFirefox向けに全面的に書き直した 135 | - 2.0.2008052801 136 | * アクションに対する機能の割り当てを初期状態にすぐに戻せるようにした 137 | * コンテキストメニューの機能が動かなくなっていたのを修正 138 | * フランス語ロケール更新 (by BlackJack) 139 | * トルコ語ロケール追加 (by Anıl Yıldız) 140 | - 2.0.2008052701 141 | * ハンガリー語ロケール追加 (by Mikes Kaszmán István) 142 | * フランス語ロケール更新 (by menet) 143 | - 2.0.2008052601 144 | * スキーマ部分を省略したURIの読み込みに失敗する場合があったのを修正 145 | * 選択範囲のURI文字列を開く際、重複分は開かないようにした 146 | - 2.0.2008050601 147 | * 台湾中国語ロケール更新 148 | - 2.0.2008042801 149 | * 古いFirefox用のコードを一部削除 150 | - 2.0.2007111301 151 | * イタリア語ロケール更新 152 | - 2.0.2007111201 153 | * イタリア語ロケールのDTDエラーを修正 154 | - 2.0.2007111103 155 | * イタリア語ロケールが正しく認識されていなかったのを修正 156 | * アドオンマネージャ用のアイコンを追加(元デザイン:Marco C.) 157 | - 2.0.2007111102 158 | * イタリア語ロケール追加(by Marco C.) 159 | - 2.0.2007111101 160 | * 複数のアクションを設定できるようにした 161 | * Minefield対応 162 | - 1.3.2007110501 163 | * 台湾中国語ロケール追加(by Alan CHENG) 164 | - 1.3.2007103002 165 | * GMailやGoogle Docsなどのリッチテキストエリアに対しては反応しないようにした 166 | - 1.3.2007103001 167 | * 中国語ロケール追加(by Carlos Oliveira) 168 | - 1.3.2007102201 169 | * [ツリー型タブ](http://piro.sakura.ne.jp/xul/_treestyletab.html)のAPIを利用して連携するようにした 170 | - 1.3.2006100702 171 | * Firefox 2.0 の大量のタブを開く前の警告に対応 172 | - 1.3.2006100701 173 | * Firefox 2.0 のタブのオーナー機能に対応 174 | - 1.3.20060328 175 | * フランス語の言語パックを修正(by menet) 176 | - 1.3.2006032701 177 | * URI文字列を選択するのみの「選択モード」を加えた 178 | * URI文字列を選択する時、前後の余計な文字列まで選択されたままになってしまうことがあったのを修正 179 | - 1.3.2006032601 180 | * Find As You TypeでURI文字列の一部が選択された状態やキャレットブラウズモードでURI文字列の中にカーソルがある場合において、EnterキーでURI文字列を読み込めるようにした 181 | * 相対パスの解釈を有効にしているとき、コンテキストメニューの項目の初期化に失敗することがあったのを修正 182 | - 1.3.2006031401 183 | * 選択範囲のURI文字列をコンテキストメニューから開く場合、部分選択されたURIも常に含めるようにした 184 | * 相対パスの解釈を有効にしているとき、スペースを空けてURI文字列の前に書かれた語句までURIの一部と見なすことがあった問題を修正 185 | * フランス語の言語パックを同梱した(made by menet) 186 | - 1.3.2006031301 187 | * 動作モードの設定を正しく保存・読み込みできなくなっていたのを修正 188 | * 相対パスと全角文字の解釈を同時に有効にするとURIの検出に失敗することがあったのを修正 189 | * タブを開く設定の時、常に全面のタブで開かれていたのを修正 190 | - 1.3.2006031201 191 | * 補完するパターンにワイルドカード(*、?)を使えるようにした(パターンを書き換えている場合、「初期値に戻す」ボタンをクリックしてください) 192 | * スキーマを省略されて「www」などから始まるURI文字列も補完できるようにした(パターンを書き換えている場合、「初期値に戻す」ボタンをクリックしてください) 193 | * 括弧を含むURIの認識精度を少し改善 194 | * 全角文字を半角文字に置換する処理のアルゴリズムを高速化した(based on implementation written by [Taken](http://taken.s101.xrea.com/blog/article.php?id=510)) 195 | * 設定パネルの構成を変更 196 | * デフォルトの設定を変更 197 | - 1.3.2006031001 198 | * 英語の言語パックのミスを修正 199 | * 複数のURI文字列を選択したときのコンテキストメニュー項目のラベルがおかしくなっていたのを修正 200 | * 「h**p」「h++p」などの形でエスケープ(?)されたURI文字列も解釈できるようにした 201 | * 解釈するスキーマなどの設定を、カンマ以外の文字でも区切れるようにした 202 | - 1.3.2006030901 203 | * 単純なダブルクリックだけではなく、Ctrlキーなどとの組み合わせもトリガーとして設定できるようにした 204 | * 新しいタブをフォアグラウンドで開くかバックグラウンドで開くかに関する設定の実装方法を変えた 205 | * 長いページでコンテキストメニューの展開やクリック位置の検出に時間がかかる問題を修正 206 | * 隠し設定でURI検索範囲の大きさを指定できるようにした(textlink.find_range_size) 207 | - 1.3.2006022101 208 | * 右クリック時のコンテキストメニューの内容を少し変更 209 | - 1.3.2005121301 210 | * クリック位置の検出を緩く判定するようにすると、却って判定が厳しくなってしまっていたのを修正 211 | - 1.3.2005070402 212 | * クリックしたURI文字列を読み込めない問題を修正 213 | * ダブルクリックでURI文字列をタブで開いたときに、URI文字列を選択するようにした 214 | - 1.3.2005070401 215 | * クリックしたURI文字列を読み込めないケースがあったのを修正 216 | - 1.3.2005062901 217 | * 一部のページでクリックしたURI文字列を読み込めないことがあったのを修正 218 | - 1.3.2005062801 219 | * XUL/Migemoを参考に、URI文字列が複数のノードに分割されていても一続きのURI文字列として認識できるようにした 220 | - 1.2.2005041901 221 | * よりセキュアな方法で内容領域へアクセスするようにした 222 | - 1.2.2005021001 223 | * URI文字列の検出処理を改善した 224 | - 1.2.2005020901 225 | * 選択範囲に含まれるURI文字列を一気に開く機能を加えた 226 | - 1.1.2005012901 227 | * 最新のMozillaでブラウザのChrome URLを取得できない問題を修正 228 | - 1.1.2004121601 229 | * 初期化処理・終了処理が正しく行われないことがある問題を修正 230 | * Movable Type 3.0の管理画面などにおいて処理に失敗する可能性があったのを修正 231 | - 1.1.2004090301 232 | * 設定パネルを作り直した 233 | * 全角英数文字も解釈できるようにした 234 | - 1.0.2004083102 235 | * 相対パスの検索・解決方法を改善したつもり 236 | - 1.0.2004083101 237 | * 括弧やピリオドなど、URL先頭・末尾の特定の文字を無視するようにした 238 | - 1.0.2004080701 239 | * スキーマの補完テーブルの最初の項目しか適用されない問題を修正 240 | - 1.0.2004080201 241 | * 相対パスの解釈とスキーマの補完を別々に設定できるようにした 242 | - 1.0.2004041101 243 | * URI文字列の検索処理を少し改善 244 | - 1.0.2004021703 245 | * リファラのブロック機能のチェックボックスの状態が保存されない問題を修正 246 | - 1.0.2004021702 247 | * リファラのブロック機能が働いていなかったのを修正 248 | - 1.0.2004021701 249 | * 公開 250 | -------------------------------------------------------------------------------- /webextensions/background/background.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | gLogContext = 'BG'; 9 | 10 | browser.runtime.onMessage.addListener((message, sender) => { 11 | if (!message || 12 | typeof message.type != 'string' || 13 | message.type.indexOf('textlink:') != 0) 14 | return; 15 | 16 | switch (message.type) { 17 | case kCOMMAND_TRY_ACTION: return (async () => { 18 | const action = detectActionFromEvent(message.event); 19 | log('action: ', action); 20 | if (action == kACTION_DISABLED) 21 | return null; 22 | 23 | message.cursor.framePos = sender.frameId; 24 | const result = await URIMatcher.matchSingle({ 25 | text: message.text, 26 | tabId: sender.tab.id, 27 | cursor: message.cursor, 28 | baseURI: message.base 29 | }); 30 | log('matchSingle result: ', result); 31 | if (!result) 32 | return null; 33 | 34 | result.action = action; 35 | if (result.uri) { 36 | if (action & kACTION_OPEN_IN_CURRENT) { 37 | browser.tabs.update(sender.tab.id, { 38 | url: result.uri 39 | }); 40 | } 41 | else if (action & kACTION_OPEN_IN_WINDOW) { 42 | browser.windows.create({ 43 | url: result.uri 44 | }); 45 | } 46 | else if (action & kACTION_OPEN_IN_TAB || action & kACTION_OPEN_IN_BACKGROUND_TAB) { 47 | browser.tabs.create({ 48 | active: !!(action & kACTION_OPEN_IN_TAB), 49 | url: result.uri, 50 | windowId: sender.tab.windowId, 51 | openerTabId: sender.tab.id 52 | }); 53 | } 54 | } 55 | return result; 56 | })(); 57 | 58 | case kCOMMAND_ACTION_FOR_URIS: 59 | if (message.action & kACTION_OPEN_IN_CURRENT) { 60 | browser.tabs.update(sender.tab.id, { 61 | url: message.uris[0] 62 | }); 63 | message.uris.slice(1).forEach((uri, index) => { 64 | browser.tabs.create({ 65 | url: uri, 66 | windowId: sender.tab.windowId, 67 | openerTabId: sender.tab.id 68 | }); 69 | }); 70 | } 71 | else if (message.action & kACTION_OPEN_IN_WINDOW) { 72 | message.uris.forEach((uri, index) => { 73 | browser.windows.create({ 74 | url: uri 75 | }); 76 | }); 77 | } 78 | else if (message.action & kACTION_OPEN_IN_TAB) { 79 | message.uris.forEach((uri, index) => { 80 | browser.tabs.create({ 81 | active: index == 0, 82 | url: uri, 83 | windowId: sender.tab.windowId, 84 | openerTabId: sender.tab.id 85 | }); 86 | }); 87 | } 88 | break; 89 | 90 | case kNOTIFY_READY_TO_FIND_URI_RANGES: 91 | initContextMenuForWaiting(sender.tab.id); 92 | break; 93 | 94 | case kCOMMAND_FIND_URI_RANGES: return (async () => { 95 | browser.tabs.sendMessage(sender.tab.id, { 96 | type: kNOTIFY_MATCH_ALL_PROGRESS, 97 | progress: 0 98 | }); 99 | await initContextMenuForWaiting(sender.tab.id); 100 | log('selection-changed', message); 101 | for (const range of message.ranges) { 102 | range.framePos = sender.frameId; 103 | } 104 | const results = await URIMatcher.matchAll({ 105 | tabId: sender.tab.id, 106 | ranges: message.ranges, 107 | baseURI: message.base, 108 | onProgress: (aProgress) => { 109 | try { 110 | const progress = Math.round(aProgress * 100); 111 | browser.tabs.sendMessage(sender.tab.id, { 112 | type: kNOTIFY_MATCH_ALL_PROGRESS, 113 | progress: progress, 114 | showInContent: configs.showProgress 115 | }); 116 | if (gLastContextTab == sender.tab.id) 117 | browser.menus.update('waiting', { 118 | title: browser.i18n.getMessage(`menu_waiting_label`, [progress]) 119 | }); 120 | } 121 | catch(e) { 122 | } 123 | } 124 | }); 125 | log('matchAll results: ', results); 126 | if (sender.tab.active && 127 | (await browser.windows.get(sender.tab.windowId)).focused) 128 | initContextMenuForURIs(results.map(result => result.uri)); 129 | return results; 130 | })(); 131 | } 132 | }); 133 | 134 | function detectActionFromEvent(event) { 135 | const baseType = event.inEditable ? 'actionInEditable' : 'action'; 136 | for (const name of Object.keys(kACTION_NAME_TO_ID)) { 137 | const base = `${baseType}_${name}_${event.type}`; 138 | if (!configs[base] || 139 | configs[`${base}_alt`] != event.altKey || 140 | configs[`${base}_ctrl`] != event.ctrlKey || 141 | configs[`${base}_meta`] != event.metaKey || 142 | configs[`${base}_shift`] != event.shiftKey) 143 | continue; 144 | return kACTION_NAME_TO_ID[name]; 145 | } 146 | return kACTION_DISABLED; 147 | } 148 | 149 | const MENU_ITEMS = [ 150 | 'openCurrent', 151 | 'openTab', 152 | 'openWindow', 153 | 'copy' 154 | ]; 155 | 156 | let gLastContextTab = 0; 157 | 158 | browser.menus.create({ 159 | id: 'waiting', 160 | title: browser.i18n.getMessage(`menu_waiting_label`, [0]), 161 | visible: false, 162 | enabled: false, 163 | contexts: ['selection'] 164 | }); 165 | browser.menus.create({ 166 | id: 'group', 167 | visible: false, 168 | contexts: ['selection'] 169 | }); 170 | for (const id of MENU_ITEMS) { 171 | browser.menus.create({ 172 | id, 173 | title: id, 174 | visible: false, 175 | contexts: ['selection'] 176 | }); 177 | browser.menus.create({ 178 | id: `grouped:${id}`, 179 | visible: false, 180 | parentId: 'group', 181 | contexts: ['selection'] 182 | }); 183 | } 184 | 185 | async function initContextMenuForWaiting(tabId) { 186 | log('initContextMenuForWaiting: hide all'); 187 | browser.menus.update('waiting', { 188 | visible: false 189 | }); 190 | browser.menus.update('group', { 191 | visible: false 192 | }); 193 | let count = 0; 194 | for (const id of MENU_ITEMS) { 195 | browser.menus.update(id, { 196 | visible: false 197 | }); 198 | browser.menus.update(`grouped:${id}`, { 199 | visible: false 200 | }); 201 | if (configs[`menu_${id}_single`] || 202 | configs[`menu_${id}_multiple`]) 203 | count++; 204 | } 205 | if (count == 0) { 206 | browser.menus.refresh(); 207 | return; 208 | } 209 | 210 | gLastContextTab = tabId; 211 | const progress = await browser.tabs.sendMessage(tabId, { 212 | type: kCOMMAND_FETCH_MATCH_ALL_PROGRESS 213 | }); 214 | browser.menus.update('waiting', { 215 | title: browser.i18n.getMessage(`menu_waiting_label`, [progress || 0]), 216 | visible: true 217 | }); 218 | browser.menus.refresh(); 219 | } 220 | 221 | function initContextMenuForURIs(uris) { 222 | browser.menus.update('waiting', { 223 | visible: false 224 | }); 225 | if (uris.length == 0) { 226 | browser.menus.refresh(); 227 | return; 228 | } 229 | 230 | const first = getShortURIString(uris[0]); 231 | const last = getShortURIString(uris[uris.length - 1]); 232 | const type = uris.length == 1 ? 'single' : 'multiple'; 233 | 234 | let visibleCount = 0; 235 | const visibility = {}; 236 | for (const id of MENU_ITEMS) { 237 | visibility[id] = configs[`menu_${id}_${type}`]; 238 | if (visibility[id]) 239 | visibleCount++; 240 | } 241 | log('initContextMenuForURIs visibleCount = ', visibleCount); 242 | if (visibleCount == 0) 243 | return; 244 | 245 | if (visibleCount > 1) { 246 | log('show group'); 247 | browser.menus.update('group', { 248 | title: browser.i18n.getMessage(`menu_group_${type}`, [uris.length, first, last]), 249 | visible: true 250 | }); 251 | } 252 | 253 | for (const id of MENU_ITEMS) { 254 | if (!visibility[id]) 255 | continue; 256 | const title = browser.i18n.getMessage(`menu_${id}_${type}`); 257 | if (visibleCount == 1) { 258 | log('show directly ', id); 259 | browser.menus.update(id, { 260 | title: browser.i18n.getMessage(`menu_direct_${type}`, [title, first, last]), 261 | visible: true 262 | }); 263 | } 264 | else { 265 | log('show in group ', id); 266 | browser.menus.update(`grouped:${id}`, { 267 | title, 268 | visible: true 269 | }); 270 | } 271 | } 272 | browser.menus.refresh(); 273 | } 274 | 275 | function getShortURIString(uri) { 276 | if (uri.length > 20) 277 | return browser.i18n.getMessage('shortURI', [uri.substring(0, 15).replace(/\.+$/, '')]); 278 | return uri; 279 | } 280 | 281 | browser.menus.onClicked.addListener((info, tab) => { 282 | switch (info.menuItemId.replace(/^grouped:/, '')) { 283 | case 'openCurrent': 284 | browser.tabs.sendMessage(tab.id, { 285 | type: kCOMMAND_ACTION_FOR_URIS, 286 | action: kACTION_OPEN_IN_CURRENT 287 | }); 288 | break; 289 | 290 | case 'openTab': 291 | browser.tabs.sendMessage(tab.id, { 292 | type: kCOMMAND_ACTION_FOR_URIS, 293 | action: kACTION_OPEN_IN_TAB 294 | }); 295 | break; 296 | 297 | case 'openWindow': 298 | browser.tabs.sendMessage(tab.id, { 299 | type: kCOMMAND_ACTION_FOR_URIS, 300 | action: kACTION_OPEN_IN_WINDOW 301 | }); 302 | break; 303 | 304 | case 'copy': 305 | browser.tabs.sendMessage(tab.id, { 306 | type: kCOMMAND_ACTION_FOR_URIS, 307 | action: kACTION_COPY 308 | }); 309 | break; 310 | } 311 | }); 312 | 313 | 314 | browser.tabs.onActivated.addListener(async (activeInfo) => { 315 | const ranges = await browser.tabs.sendMessage(activeInfo.tabId, { 316 | type: kCOMMAND_FETCH_URI_RANGES 317 | }); 318 | initContextMenuForURIs(ranges.map(result => result.uri)); 319 | }); 320 | 321 | browser.windows.onFocusChanged.addListener(async (windowId) => { 322 | const window = await browser.windows.get(windowId, { populate: true }); 323 | if (!window.focused) 324 | return; 325 | 326 | const activeTab = window.tabs.filter(tab => tab.active)[0]; 327 | const ranges = await browser.tabs.sendMessage(activeTab.id, { 328 | type: kCOMMAND_FETCH_URI_RANGES 329 | }); 330 | initContextMenuForURIs(ranges.map(result => result.uri)); 331 | }); 332 | -------------------------------------------------------------------------------- /webextensions/content_scripts/content.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | gLogContext = 'content'; 9 | 10 | const BOUNDARY_INLINE_NODE = 'textlink-boundary-inline-node'; 11 | 12 | let gTryingAction = false; 13 | let gLastActionResult = null; 14 | let gMatchAllProgress = 0; 15 | 16 | async function onDblClick(event) { 17 | if (event.target.ownerDocument != document) 18 | return; 19 | const data = getSelectionEventData(event); 20 | if (!data) 21 | return; 22 | gTryingAction = true; 23 | gLastActionResult = null; 24 | const textFieldSelection = isInputField(event.target); 25 | // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1806291 26 | for (const node of data.boundaryInlineNodes) { 27 | node.classList.add(BOUNDARY_INLINE_NODE); 28 | } 29 | gLastActionResult = await browser.runtime.sendMessage({ 30 | ...data, 31 | boundaryInlineNodes: [], // don't send raw DOM nodes 32 | type: kCOMMAND_TRY_ACTION 33 | }); 34 | for (const node of data.boundaryInlineNodes) { 35 | node.classList.remove(BOUNDARY_INLINE_NODE); 36 | } 37 | if (textFieldSelection && 38 | gLastActionResult && 39 | gLastActionResult.range) 40 | gLastActionResult.range.fieldNodePos = getFieldNodePosition(event.target); 41 | postAction(gLastActionResult); 42 | await wait(500); 43 | gTryingAction = false; 44 | } 45 | 46 | function onKeyDownThrottled(event) { 47 | if (event.target.ownerDocument != document || 48 | event.key != 'Enter') 49 | return; 50 | if (onKeyDownThrottled.timeout) 51 | clearTimeout(onKeyDownThrottled.timeout); 52 | onKeyDownThrottled.timeout = setTimeout(() => { 53 | onKeyDownThrottled.timeout = null; 54 | onKeyDown(event); 55 | }, 100); 56 | } 57 | onKeyDownThrottled.timeout = null; 58 | 59 | async function onKeyDown(event) { 60 | const data = getSelectionEventData(event); 61 | if (!data) 62 | return; 63 | gTryingAction = true; 64 | gLastActionResult = null; 65 | const textFieldSelection = isInputField(event.target); 66 | // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1806291 67 | for (const node of data.boundaryInlineNodes) { 68 | node.classList.add(BOUNDARY_INLINE_NODE); 69 | } 70 | gLastActionResult = await browser.runtime.sendMessage({ 71 | ...data, 72 | boundaryInlineNodes: [], // don't send raw DOM nodes 73 | type: kCOMMAND_TRY_ACTION 74 | }); 75 | for (const node of data.boundaryInlineNodes) { 76 | node.classList.remove(BOUNDARY_INLINE_NODE); 77 | } 78 | if (textFieldSelection && 79 | gLastActionResult && 80 | gLastActionResult.range) 81 | gLastActionResult.range.fieldNodePos = getFieldNodePosition(event.target); 82 | postAction(gLastActionResult); 83 | gTryingAction = false; 84 | } 85 | 86 | function postAction(result) { 87 | if (!result) 88 | return; 89 | 90 | if (result.action & kACTION_COPY) 91 | doCopy(result.uri); 92 | if (result.range) 93 | selectRanges(result.range); 94 | } 95 | 96 | function doCopy(text) { 97 | gChangingSelectionRangeInternally++; 98 | const selection = window.getSelection(); 99 | const ranges = []; 100 | for (let i = 0, maxi = selection.rangeCount; i < maxi; i++) { 101 | ranges.push(getRangeData(selection.getRangeAt(i))); 102 | } 103 | 104 | // this is required to block overriding clipboard data from scripts of the webpage. 105 | document.addEventListener('copy', event => { 106 | event.stopImmediatePropagation(); 107 | event.preventDefault(); 108 | event.clipboardData.setData('text/plain', text); 109 | }, { 110 | capture: true, 111 | once: true 112 | }); 113 | 114 | const field = document.createElement('textarea'); 115 | field.value = text; 116 | document.body.appendChild(field); 117 | field.style.position = 'fixed'; 118 | field.style.opacity = 0; 119 | field.style.pointerEvents = 'none'; 120 | field.focus(); 121 | field.select(); 122 | document.execCommand('copy'); 123 | field.parentNode.removeChild(field); 124 | 125 | selectRanges(ranges); 126 | gChangingSelectionRangeInternally--; 127 | } 128 | 129 | 130 | let gLastSelection = ''; 131 | let gFindingURIRanges = false; 132 | let gLastSelectionChangeAt = 0; 133 | let gLastURIRanges = Promise.resolve([]); 134 | var gChangingSelectionRangeInternally = 0; 135 | 136 | async function onSelectionChange(event) { 137 | if (gChangingSelectionRangeInternally > 0) 138 | return; 139 | 140 | const changedAt = gLastSelectionChangeAt = Date.now(); 141 | if (findURIRanges.delayed) 142 | clearTimeout(findURIRanges.delayed); 143 | 144 | await wait(200); 145 | if (changedAt != gLastSelectionChangeAt) 146 | return; 147 | 148 | if (gTryingAction) { 149 | while (gTryingAction) { 150 | await wait(500); 151 | } 152 | if (changedAt != gLastSelectionChangeAt) 153 | return; 154 | if (gLastActionResult) { 155 | gLastURIRanges = Promise.resolve([gLastActionResult]); 156 | return; 157 | } 158 | } 159 | 160 | if (isInputField(event.target)) 161 | onTextFieldSelectionChanged(event.target); 162 | else 163 | onSelectionRangeChanged(); 164 | } 165 | 166 | function onTextFieldSelectionChanged(field) { 167 | const selectionRange = getFieldRangeData(field); 168 | 169 | gLastSelection = selectionRange.text.substring(selectionRange.startOffset, selectionRange.endOffset); 170 | gFindingURIRanges = true; 171 | 172 | browser.runtime.sendMessage({ 173 | type: kNOTIFY_READY_TO_FIND_URI_RANGES 174 | }); 175 | gLastURIRanges = new Promise(async (reolve, reject) => { 176 | const ranges = await browser.runtime.sendMessage({ 177 | type: kCOMMAND_FIND_URI_RANGES, 178 | base: location.href, 179 | ranges: [selectionRange] 180 | }); 181 | const position = getFieldNodePosition(field); 182 | for (const range of ranges) { 183 | range.range.fieldNodePos = position; 184 | } 185 | gFindingURIRanges = false; 186 | reolve(ranges); 187 | }); 188 | } 189 | 190 | function onSelectionRangeChanged() { 191 | const selection = window.getSelection(); 192 | const selectionText = selection.toString() 193 | if (selectionText == gLastSelection) 194 | return; 195 | 196 | gLastSelection = selectionText; 197 | gFindingURIRanges = true; 198 | 199 | browser.runtime.sendMessage({ 200 | type: kNOTIFY_READY_TO_FIND_URI_RANGES 201 | }); 202 | 203 | findURIRanges.delayed = setTimeout(() => { 204 | delete findURIRanges.delayed; 205 | gLastURIRanges = findURIRanges(); 206 | }, 100); 207 | } 208 | 209 | async function findURIRanges(options = {}) { 210 | const selection = window.getSelection(); 211 | if (!selection.toString().trim()) { 212 | browser.runtime.sendMessage({ 213 | type: kCOMMAND_FIND_URI_RANGES, 214 | base: location.href, 215 | ranges: [] 216 | }); 217 | return []; 218 | } 219 | 220 | clearNodeVisibilityCache(); 221 | 222 | const selectionRanges = []; 223 | const boundaryInlineNodes = []; 224 | for (let i = 0, maxi = selection.rangeCount; i < maxi; i++) { 225 | const selectionRange = selection.getRangeAt(i); 226 | const selectionText = rangeToText(selectionRange); 227 | const precedings = getPrecedingRanges(selectionRange); 228 | const followings = getFollowingRanges(selectionRange); 229 | const rangeData = getRangeData(selectionRange); 230 | rangeData.text = selectionText.text; 231 | rangeData.expandedText = `${precedings.texts.join('')}${selectionText.text}${followings.texts.join('')}`; 232 | selectionRanges.push(rangeData); 233 | boundaryInlineNodes.push(...selectionText.boundaryInlineNodes, ...precedings.boundaryInlineNodes, ...followings.boundaryInlineNodes); 234 | } 235 | // workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1806291 236 | for (const node of boundaryInlineNodes) { 237 | node.classList.add(BOUNDARY_INLINE_NODE); 238 | } 239 | const ranges = await browser.runtime.sendMessage({ 240 | type: kCOMMAND_FIND_URI_RANGES, 241 | base: location.href, 242 | ranges: selectionRanges 243 | }); 244 | for (const node of boundaryInlineNodes) { 245 | node.classList.remove(BOUNDARY_INLINE_NODE); 246 | } 247 | gFindingURIRanges = false; 248 | return ranges; 249 | } 250 | 251 | function getSelectionEventData(event) { 252 | const textFieldSelection = isInputField(event.target); 253 | 254 | const selection = window.getSelection(); 255 | if (!textFieldSelection && selection.rangeCount != 1) 256 | return null; 257 | 258 | clearNodeVisibilityCache(); 259 | 260 | let text, cursor, boundaryInlineNodes; 261 | if (textFieldSelection) { 262 | cursor = getFieldRangeData(event.target); 263 | text = cursor.text; 264 | boundaryInlineNodes = []; 265 | } 266 | else { 267 | const selectionRange = selection.getRangeAt(0); 268 | const selectionText = rangeToText(selectionRange); 269 | const precedings = getPrecedingRanges(selectionRange); 270 | const followings = getFollowingRanges(selectionRange); 271 | text = `${precedings.texts.join('')}${selectionText.text}${followings.texts.join('')}`; 272 | cursor = getRangeData(selectionRange); 273 | boundaryInlineNodes = [...selectionText.boundaryInlineNodes, ...precedings.boundaryInlineNodes, ...followings.boundaryInlineNodes]; 274 | } 275 | 276 | const data = { 277 | text, cursor, 278 | base: location.href, 279 | event: { 280 | altKey: event.altKey, 281 | ctrlKey: event.ctrlKey, 282 | metaKey: event.metaKey, 283 | shiftKey: event.shiftKey, 284 | inEditable: textFieldSelection || isEditableNode(event.target) 285 | }, 286 | boundaryInlineNodes, 287 | }; 288 | 289 | if (event.type == 'dblclick' && 290 | event.button == 0) { 291 | data.event.type = 'dblclick'; 292 | } 293 | else if (event.type == 'keydown' && 294 | event.key == 'Enter') { 295 | data.event.type = 'enter'; 296 | } 297 | else { 298 | return null; 299 | } 300 | return data; 301 | } 302 | 303 | 304 | function onFocused(event) { 305 | const node = event.target; 306 | if (!isInputField(node)) 307 | return; 308 | node.addEventListener('selectionchange', onSelectionChange, { capture: true }); 309 | window.addEventListener('unload', () => { 310 | node.removeEventListener('selectionchange', onSelectionChange, { capture: true }); 311 | }, { once: true }); 312 | } 313 | 314 | function isInputField(node) { 315 | return ( 316 | node.nodeType == Node.ELEMENT_NODE && 317 | evaluateXPath(`self::*[${kFIELD_CONDITION}]`, node, XPathResult.BOOLEAN_TYPE).booleanValue 318 | ); 319 | } 320 | 321 | function isEditableNode(node) { 322 | if ((node.ownerDocument || node).designMode == 'on') 323 | return true; 324 | while (node) { 325 | if (node.contentEditable == 'true') 326 | return true; 327 | node = node.parentNode; 328 | } 329 | return false; 330 | } 331 | 332 | 333 | function onMessage(aMessage, aSender) { 334 | switch (aMessage.type) { 335 | case kCOMMAND_ACTION_FOR_URIS: return (async () => { 336 | let ranges = await gLastURIRanges; 337 | if (aMessage.action & kACTION_COPY) { 338 | let uris = ranges.map(aRange => aRange.uri).join('\n'); 339 | if (ranges.length > 1) 340 | uris += '\n'; 341 | doCopy(uris); 342 | } 343 | else { 344 | browser.runtime.sendMessage({ 345 | ...aMessage, 346 | uris: ranges.map(aRange => aRange.uri) 347 | }); 348 | } 349 | if (ranges.length > 0 && 350 | (!('startTextNodePos' in ranges[0]) || 351 | !('endTextNodePos' in ranges[0]))) { 352 | gLastURIRanges = findURIRanges(); 353 | ranges = await gLastURIRanges; 354 | } 355 | selectRanges(ranges.map(aRange => aRange.range)); 356 | })(); 357 | 358 | case kCOMMAND_FETCH_URI_RANGES: 359 | return gLastURIRanges; 360 | 361 | case kNOTIFY_MATCH_ALL_PROGRESS: 362 | gMatchAllProgress = aMessage.progress; 363 | if (aMessage.showInContent) 364 | updateProgress(); 365 | break; 366 | 367 | case kCOMMAND_FETCH_MATCH_ALL_PROGRESS: 368 | return Promise.resolve(gMatchAllProgress); 369 | } 370 | } 371 | 372 | let gProgressIndicator; 373 | 374 | function updateProgress() { 375 | if (gMatchAllProgress >= 100 || gMatchAllProgress <= 0) { 376 | if (gProgressIndicator) { 377 | gProgressIndicator.parentNode.removeChild(gProgressIndicator); 378 | gProgressIndicator = null; 379 | } 380 | return; 381 | } 382 | 383 | if (!gProgressIndicator) { 384 | const range = document.createRange(); 385 | range.selectNodeContents(document.body || document.documentElement); 386 | range.collapse(false); 387 | const fragment = range.createContextualFragment(``); 402 | gProgressIndicator = fragment.firstChild; 403 | range.insertNode(fragment); 404 | range.detach(); 405 | } 406 | gProgressIndicator.style.background = ` 407 | linear-gradient(to right, 408 | #24b7b7 0%, 409 | #85f2e1 ${gMatchAllProgress}%, 410 | #000000 ${gMatchAllProgress}%, 411 | #000000 100%) 412 | `; 413 | gProgressIndicator.setAttribute('title', browser.i18n.getMessage('menu_waiting_label', [gMatchAllProgress])); 414 | } 415 | 416 | window.addEventListener('dblclick', onDblClick, { capture: true }); 417 | window.addEventListener('keydown', onKeyDownThrottled, { capture: true }); 418 | window.addEventListener('selectionchange', onSelectionChange, { capture: true }); 419 | window.addEventListener('focus', onFocused, { capture: true }); 420 | browser.runtime.onMessage.addListener(onMessage); 421 | 422 | window.addEventListener('unload', () => { 423 | window.removeEventListener('dblclick', onDblClick, { capture: true }); 424 | window.removeEventListener('keydown', onKeyDownThrottled, { capture: true }); 425 | window.removeEventListener('selectionchange', onSelectionChange, { capture: true }); 426 | window.removeEventListener('focus', onFocused, { capture: true }); 427 | browser.runtime.onMessage.removeListener(onMessage); 428 | }, { once: true }); 429 | 430 | -------------------------------------------------------------------------------- /history.en.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | - master/HEAD 4 | - 6.1.10 (2025.5.27) 5 | * Detect multiple URIs in a selection correctly. (regression at 6.1.5) 6 | * Detect exactly targetted URI from partial selection even if there is any shorter version URIs appear before the targetted part. 7 | - 6.1.9 (2023.3.25) 8 | * Optimize reaction for modifications of text selections: early termination of DOM traversing on not visually inline container. 9 | - 6.1.8 (2023.3.24) 10 | * Optimize reaction for modifications of text selections: early termination of DOM traversing on switching of block level container. 11 | - 6.1.7 (2023.1.7) 12 | * Support URL texts separated by virtual inline elements (e.g. URL text including screen name links on Twitter). 13 | * Optimize performance to check node visibility. 14 | * Update the list of effective TLDs (based on [public suffix list](https://publicsuffix.org/).) 15 | - 6.1.6 (2021.12.21) 16 | * Trigger actions with keydown events instead of keyup. This reduces misfirering of Ctrl-Enter by immediately pressed Ctrl-V for a text including URL just after regular Enter. 17 | * Update the list of effective TLDs (based on [public suffix list](https://publicsuffix.org/).) 18 | - 6.1.5 (2021.11.5) 19 | * Detect only the single long URI from a selection range on itself, instead of both partial URIs and the long URI, if there are any partial URIs preceding to the selection range. 20 | - 6.1.4 (2021.11.4) 21 | * Fix wrong URI detection around context menu commands. 22 | - 6.1.3 (2021.10.25) 23 | * Fix non-functioning of the context menu item (by [gontazaka](https://github.com/gontazaka), thanks!) 24 | - 6.1.2 (2020.5.5) 25 | * Use the [public suffix list](https://publicsuffix.org/) as the source of known top level domains. URL-like strings including a hostname with a top level domain included in the list are detected as effective URLs now. 26 | * Fix wrong behaviors of "All Configs" UI: apply imported configs to options UI immediately and treat decimal values as valid for some numeric options. 27 | - 6.1.1 (2020.6.8) 28 | * Fix unexpected slow-down of keyboard input in text fields. 29 | - 6.1.0 (2020.3.10) 30 | * Fix invisible context menu commands on text selection. 31 | * Fix broken options page on lately versions Firefox. 32 | * Drop support for Firefox 63 and older versions. 33 | - 6.0.3 (2019.8.8) 34 | * Remove obsolete codes deprecated at Firefox 70. 35 | * Add ability to export and import all configurations except keyboard shortcuts. (Options => "Development" => "Debug mode" => "All Configs" => "Import/Export") 36 | - 6.0.2 (2018.7.30) 37 | * Optimize operations to select detected URIs. In old versions, the operation took too much time on a webpage with too much nodes. 38 | * Open clicked URI correctly even if the page contents are modified while processing. 39 | * Synchronize configurations by Firefox Sync. 40 | - 6.0.1 (2017.11.4) 41 | * Optimize operations to detect URIs from selection. Now actual ranges are not detected until they are actuall required. 42 | - 6.0.0 (2017.11.3) 43 | * Rebuilt on WebExtensions. 44 | * Drop support for Thunderbird. 45 | - 5.0.2016031501 46 | * Works on Nightly 48.0a1. 47 | * Manages custom preferences under its own namespace. 48 | * Open all URIs correctly when the menu item "Open ... in Tabs" is selected immediately. 49 | * Update pl locale ([by Piotr Drąg, thanks!](https://github.com/piroor/textlink/pull/52)) 50 | * Deactivate context menu items correctly when there is no URI in the selection. 51 | * Deactivate tooltip on disabled menu items in the context menu. 52 | - 5.0.2015060501 53 | * Works correctly on the multi-process mode (E10S). 54 | * Improved: Add a new locale hy-AM (Armenian), translated by [Hrant Ohanyan](http://haysoft.org). Thanks! 55 | - 4.1.2013040601 56 | * Fixed: Some odd behaviors around selection range are corrected. 57 | * Modified: "jar" archive is no longer included. 58 | - 4.1.2012122901 59 | * Works on Nightly 20.0a1. 60 | * Drop support for Firefox 9 and older versions. 61 | * Improved: For context menu features, now URIs in the selection range are detected progressively. The context menu on web pages are shown quickly. 62 | * Improved: Add support for URIs with port number. 63 | * Fixed: Load selected single URI into the current tab correctly. 64 | * Fixed: Detect URI from selection correctly, for some edge cases. 65 | - 4.0.2011021601 66 | * Fixed: Migration of preferences was failed if user had custom values. 67 | - 4.0.2011021301 68 | * Improved: IDN recognition can be activated for specific schemes ("http", "https", "ftp", "news", "nntp", "telnet" and "irc" by default). 69 | * Improved: "data:" URIs can be recognized by default. 70 | * Fixed: URLs includes inline password (like "user:pass@domain") can be recognized again. 71 | * Fixed: URIs without valid TLD (about: URIs, chrome: URLs and so on) can be recognized again. 72 | * Fixed: Context menu items were always hidden on input fields unexpectedly. 73 | * Fixed: URI detecton was failed when the selection range contains any input field. 74 | - 4.0.2011020501 75 | * Fixed: "Open selected URIs in tabs" feature didn't work on Minefield. 76 | - 4.0.2011012101 77 | * Works on Minefield 4.0b10pre. 78 | * Drop support for Firefox 3.0 and older versions. 79 | * Works on Thunderbird 3.1. 80 | * Drop support for Thunderbird 2 and older versions. 81 | * Improved: IDN (Internationalized Domain Names) can be recognized. 82 | * Fixed: Too slow context menu on web pages with no selection disappeared. 83 | * German locale is added, by Michael Baer. 84 | - 3.1.2009110201 85 | * Improved: Built-in TLD list is updated. 86 | * Fixed: Keyboard events are ignored except Enter (Return) key. 87 | * Fixed: Safer code. 88 | * French locale is updated. (by menet) 89 | * Turkish locale is updated. (by An娼ナl Y娼ナld娼ナz) 90 | - 3.1.2009032701 91 | * Fixed: Open-parenthesis after URI strings are correctly ignored. 92 | * Fixed: Some links wrongly linkified by Thunderbird are correctly unlinkified. 93 | - 3.1.2009032601 94 | * Improved: Works on Thunderbird too. 95 | * Fixed: URI strings like "URL:http://..." are correctly detected. 96 | * Updated: zh-TW locale is updated by by Alan CHENG. 97 | - 3.0.2009031801 98 | * Optimized. 99 | - 3.0.2009031701 100 | * Improved: Works faster on Firefox 3. 101 | * Fixed: Building process of the tooltip stops correctly after the tooltip was hidden. 102 | * Fixed: Text nodes in different block-level elements are recognized as split texts. 103 | - 3.0.2009030901 104 | * Fixed: Works on Firefox 2 correctly. 105 | * Fixed: Double-click and other actions for single URI in plain text work correctly. 106 | * Improved: Less time to show context menu. 107 | - 3.0.2009022402 108 | * Fixed: Performance problem on long webpages mostly disappeared. 109 | * Fixed: URI-like strings out of selections are ignored correctly. 110 | - 3.0.2009022401 111 | * Fixed: Works on plain text files correctly. 112 | * Fixed: Works on pages which have hidden texts. 113 | * Modified: Both full-width characters "¥u301c" and "¥uff5e" are regarded as "‾". 114 | - 3.0.2009021901 115 | * Fixed: Mistake in default rule of URI fixup for domains which start with "www." or "ftp." is corrected. 116 | * Turkish locale is updated. (by An娼ナl Y娼ナld娼ナz) 117 | - 3.0.2009021801 118 | * Fixed: Operations on webpages which have many short text are optimized. 119 | - 3.0.2009021601 120 | * Improved: You can open URIs from text fields by the context menu. 121 | * Fixed: Freezing on GMail disappeared. (URI-like strings in hidden elements, ex. <style> and others, are now ignored.) 122 | - 3.0.2009021502 123 | * Fixed: Strings next to <BR> just after URI string are ignored correctly. In old versions, a wrong URI "http://example.com/foobar" was detected from HTML "http://example.com/<BR>foobar". 124 | * Modified: Preceding invalid characters of an URI string are ignored if parsing of relative path is disabled. 125 | * French locale is updated. (by menet) 126 | - 3.0.2009021401 127 | * Improved: "Copy selected URIs" feature is available. 128 | * Improved: "Copy clicked URI" is available for clicking on URI strings. 129 | * Improved: Multiple selections are available on Firefox 3 or later. 130 | * Improved: All of detected URIs in the selection are shown in a tooltip on context menu items. 131 | * Fixed: Just URI string is selected when it is clicked. Parenthesis around URIs are never selected. 132 | - 2.1.2009021301 133 | * Drop support of Firefox older than Firefox 2. 134 | * Configuration dialog is totally rewritten for modern Firefox. 135 | - 2.0.2008052801 136 | * Improved: New "Reset" button is available to get default settings back. 137 | * Fixed: Features in the context menu work correctly. 138 | * French locale is updated. (by BlackJack) 139 | * Turkish locale is available. (by An娼ナl Y娼ナld娼ナz) 140 | - 2.0.2008052701 141 | * Hungarian locale is available. (by Mikes Kaszm将。n Istv将。n) 142 | * French locale is updated. (by menet) 143 | - 2.0.2008052601 144 | * Fixed: Domain names without schemer part are loaded correctly. 145 | * Fixed: Duplicated URIs are ignored for "Open Selection URIs" feature. 146 | - 2.0.2008050601 147 | * Updated: Traditional Chinese locale is updated. 148 | - 2.0.2008042801 149 | * Modified: Some obsolete codes are removed. 150 | - 2.0.2007111301 151 | * Updated: Italian locale is updated. 152 | - 2.0.2007111201 153 | * Fixed: DTD error in Italian locale is fixed. 154 | - 2.0.2007111103 155 | * Fixed: Italian locale is detected correctly. 156 | * Added: Icon for add-on manager is available. (original designed by Marco C.) 157 | - 2.0.2007111102 158 | * Added: Italian locale is available. (made by Marco C.) 159 | - 2.0.2007111101 160 | * Improved: Multiple actions can be defined. 161 | * Improved: Works on Minefield. 162 | - 1.3.2007110501 163 | * Added: Traditional Chinese locale is available. (made by Alan CHENG) 164 | - 1.3.2007103002 165 | * Fixed: Ignores URIs in rich text area like GMail, Google Docs, etc. 166 | - 1.3.2007103001 167 | * Added: Chinese locale is available. (made by Carlos Oliveira) 168 | - 1.3.2007102201 169 | * Improved: Works with [Tree Style Tab](http://piro.sakura.ne.jp/xul/_treestyletab.html.en). 170 | - 1.3.2006100702 171 | * Improved: Warning for too many tabs to be opened is supported in Firefox 2.0. 172 | - 1.3.2006100701 173 | * Improved: The "tab owner" feauter of Firefox 2.0 is supported. 174 | - 1.3.20060328 175 | * The French language pack is corrected and updated. (by menet) 176 | - 1.3.2006032701 177 | * Improved: A new mode, "Select Mode" is available. In this mode, URI strings are only selected, aren't loaded. 178 | * Fixed: URI strings are correctly selected. 179 | - 1.3.2006032601 180 | * Improved: Pressing Enter key loads URI text when a part of URI is highlighted by "Find As You Type" or in the Caret-browsing mode. 181 | * Fixed: Broken context menu is corrected. 182 | - 1.3.2006031401 183 | * Improved: "Open in New Tabs" and other extra features in the context menu always load partly selected URIs too. 184 | * Fixed: Mis-detection for URIs which are placed after English terms disappeared. 185 | * Improved: French Language Pack available. (made by menet) 186 | - 1.3.2006031301 187 | * Fixed: Mode setting can be loaded/saved correctly. 188 | * Fixed: URI texts are detected correctly even if both relative pathes and full-width characters are available. 189 | * Fixed: URIs are loaded into a background tab correctly by your setting. 190 | - 1.3.2006031201 191 | * Improved: Wildcards ("*" and "?") are available in the patterns of fixing up broken URIs. Please click "Reset" button in the dialog if you are using a customized pattern. 192 | * Improved: URIs start with "www" or other patterns without schemer can be recognized. Please click "Reset" button in the dialog if you are using a customized pattern. 193 | * Improved: URIs including parenthesis ("(" or ")") are recognized more correctly. 194 | * Improved: The algorithm to convert full-width characters to half-width characters is optimized. (based on implementation written by [Taken](http://taken.s101.xrea.com/blog/article.php?id=510)) 195 | * Modified: The configuration dialog is restructured. 196 | * Modified: The default settings are modified. 197 | - 1.3.2006031001 198 | * Fixed: An mistake in the English Language Pack disappeared. 199 | * Fixed: Broken label of an extra menuitem in the context menu for multiple URI texts is corrected. 200 | * Improved: URI strings start with "h**p" and "h++p" can be recognized. 201 | * Improved: Some characters not only "," are available to separete values of preference settings. 202 | - 1.3.2006030901 203 | * Improved: Modifier keys are available to load clicked URI text. 204 | * Modified: The setting to change which to load new tabs in in the foreground or the background. 205 | * Fixed: The large delay of showing the context menu on webpages disappeared. 206 | * Improved: The size of the searching range for the clicked point can be customizable with a secret setting: "textlink.find_range_size". 207 | - 1.3.2006022101 208 | * Improved: Extra menu items for the context menu on web pages are improved. 209 | - 1.3.2005121301 210 | * Fixed: Works correctly even if in the loose mode. 211 | - 1.3.2005070402 212 | * Fixed: Clicked URI strings are loaded correctly for any case. 213 | * Improved: Double-clicked URI strings are selected automatically. 214 | - 1.3.2005070401 215 | * Fixed: Clicked URI strings are loaded correctly for any case. 216 | - 1.3.2005062901 217 | * Fixed: Fatal error for some webpages disappeared. 218 | - 1.3.2005062801 219 | * URI strings made from multiple nodes can be parsed as URI strings correctly. The implementation of XUL/Migemo gives me this idea. 220 | - 1.2.2005041901 221 | * Modified: Codes to access content area become secure. 222 | - 1.2.2005021001 223 | * Improved: URI strings are detected more precisely. 224 | - 1.2.2005020901 225 | * Improved: A new feature to open all of URI strings in the selection is available. 226 | - 1.1.2005012901 227 | * Fixed: An fatal error about getting chrome URL of the default browser in the lately Firefox disappeared. 228 | - 1.1.2004121601 229 | * Fixed: Errors on initializing and closing disappeared. 230 | * Fixed: Possibility of errors on some webpages (Movable Type 3.0, etc.) disappeared. 231 | - 1.1.2004090301 232 | * Improved: New configuration dialog is available. 233 | * Improved: Multi-byte characters are supported. 234 | - 1.0.2004083102 235 | * Fixed: Relative links are parsed more correctly 236 | - 1.0.2004083101 237 | * Fixed: Some characters ("(", ")", ".", and ",") are ignored when they are in the start or the end of URL strings. 238 | - 1.0.2004080701 239 | * Fixed: Table of schemers fixed up are completely available. 240 | - 1.0.2004080201 241 | * Improved: Two options are available instead of the old autocomplete option. 242 | - 1.0.2004041101 243 | * Improved: Patterns matching to URIs have been brushed up. 244 | - 1.0.2004021703 245 | * Fixed: Checkbox for HTTP_REFERER blocking has worked correctly. 246 | - 1.0.2004021702 247 | * Fixed: HTTP_REFERER has been blocked correctly. 248 | - 1.0.2004021701 249 | * Released. 250 | -------------------------------------------------------------------------------- /webextensions/content_scripts/range.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | const STATE_CONTINUE_PHYSICALLY = 1 << 0; 9 | const STATE_CONTINUE_VISUALLY = 1 << 1; 10 | const STATE_SEPARATED = 1 << 2; 11 | 12 | function rangeToText(range) { 13 | const walker = createVisibleTextNodeWalker(); 14 | walker.currentNode = range.startContainer; 15 | let result = ''; 16 | if (walker.currentNode.nodeType == Node.TEXT_NODE) { 17 | let text = walker.currentNode.nodeValue; 18 | if (walker.currentNode == range.endContainer) 19 | text = text.substring(0, range.endOffset); 20 | text = text.substring(range.startOffset); 21 | result += text; 22 | } 23 | 24 | const boundaryInlineNodes = []; 25 | while (walker.nextNode()) { 26 | const node = walker.currentNode; 27 | const position = range.endContainer.compareDocumentPosition(node); 28 | if (position & Node.DOCUMENT_POSITION_FOLLOWING && 29 | !(position & Node.DOCUMENT_POSITION_CONTAINED_BY)) 30 | break; 31 | if (node == range.endContainer) { 32 | if (node.nodeType == Node.TEXT_NODE) { 33 | const text = node.nodeValue.substring(0, range.endOffset); 34 | result += text; 35 | } 36 | break; 37 | } 38 | const { text, state } = nodeToText(node); 39 | result += text; 40 | if (state == STATE_CONTINUE_VISUALLY) 41 | boundaryInlineNodes.push(node.offsetParent); 42 | } 43 | 44 | return { 45 | text: result, // .replace(/\n\s*|\s*\n/g, '\n') 46 | boundaryInlineNodes, 47 | }; 48 | } 49 | 50 | function nodeToText(node) { 51 | if (node.nodeType != Node.ELEMENT_NODE) 52 | return { 53 | text: node.nodeValue, 54 | state: STATE_CONTINUE_PHYSICALLY, 55 | }; 56 | 57 | if (/^br$/i.test(String(node.localName))) 58 | return { 59 | text: '\n', 60 | state: STATE_SEPARATED, 61 | }; 62 | 63 | if (/^inline/.test(window.getComputedStyle(node, null).display)) 64 | return { 65 | text: '', 66 | state: STATE_CONTINUE_PHYSICALLY, 67 | }; 68 | 69 | 70 | // We should treat inline-* block as virtual inline node, only when it is visually inline really. 71 | // For example, if it is a inlie-block but has 100% width of its container, it may not look as an inline block. 72 | // See also: https://github.com/piroor/textlink/issues/80 73 | const offsetParent = node.offsetParent; 74 | const containerWidth = offsetParent && parseFloat(window.getComputedStyle(findNearestContainerElement(offsetParent), null).width); 75 | if (offsetParent && 76 | isInline(offsetParent, { containerWidth }) && 77 | (isInline(findEffectivePreviousSibling(offsetParent), { containerWidth }) || 78 | isInline(findEffectiveNextSibling(offsetParent), { containerWidth }) || 79 | isInline(offsetParent.parentNode, { containerWidth }))) 80 | return { 81 | text: '', 82 | state: STATE_CONTINUE_VISUALLY, 83 | }; 84 | 85 | return { 86 | text: '\n', 87 | state: STATE_SEPARATED, 88 | }; 89 | } 90 | 91 | function isInline(node, { containerWidth } = {}) { 92 | if (!node) 93 | return false; 94 | 95 | if (node.nodeType == Node.TEXT_NODE) 96 | return true; 97 | 98 | if (node.nodeType != Node.ELEMENT_NODE) 99 | return false; 100 | 101 | const display = window.getComputedStyle(node, null).display; 102 | if (display == 'inline' || !containerWidth) 103 | return true; 104 | 105 | return /^inline-/.test(display) && (node.getBoundingClientRect().width < containerWidth); 106 | } 107 | 108 | function findEffectivePreviousSibling(node) { 109 | if (!node) 110 | return null; 111 | 112 | const sibling = node.previousSibling; 113 | switch (sibling.nodeType) { 114 | case Node.TEXT_NODE: 115 | if (sibling.nodeValue.trim() == '') 116 | return findEffectivePreviousSibling(sibling); 117 | break; 118 | 119 | case Node.ELEMENT_NODE: 120 | return sibling; 121 | 122 | default: 123 | return findEffectivePreviousSibling(sibling); 124 | } 125 | } 126 | 127 | function findEffectiveNextSibling(node) { 128 | if (!node) 129 | return null; 130 | 131 | const sibling = node.nextSibling; 132 | switch (sibling.nodeType) { 133 | case Node.TEXT_NODE: 134 | if (sibling.nodeValue.trim() == '') 135 | return findEffectiveNextSibling(sibling); 136 | break; 137 | 138 | case Node.ELEMENT_NODE: 139 | return sibling; 140 | 141 | default: 142 | return findEffectiveNextSibling(sibling); 143 | } 144 | } 145 | 146 | function getPrecedingRanges(sourceRange) { 147 | const texts = []; 148 | const ranges = []; 149 | const boundaryInlineNodes = []; 150 | const range = document.createRange(); 151 | range.setStart(sourceRange.startContainer, sourceRange.startOffset); 152 | range.setEnd(sourceRange.startContainer, sourceRange.startOffset); 153 | 154 | const scanningBoundaryPoint = range.cloneRange(); 155 | scanningBoundaryPoint.selectNodeContents(findNearestContainerElement(sourceRange.startContainer)); 156 | scanningBoundaryPoint.collapse(true); 157 | const scanningRange = range.cloneRange(); 158 | 159 | const walker = createVisibleTextNodeWalker(); 160 | walker.currentNode = range.startContainer; 161 | let text = ''; 162 | if (walker.currentNode.nodeType == Node.TEXT_NODE) { 163 | text += walker.currentNode.nodeValue.substring(0, range.startOffset); 164 | } 165 | else { 166 | const previousNode = walker.currentNode.childNodes[range.startOffset]; 167 | if (previousNode) 168 | walker.currentNode = previousNode; 169 | } 170 | while (walker.previousNode()) { 171 | // We should accept text nodes in the same container. 172 | scanningRange.selectNode(walker.currentNode); 173 | if (scanningRange.compareBoundaryPoints(Range.START_TO_END, scanningBoundaryPoint) <= 0) { 174 | //console.log('getPrecedingRanges: reached to the boundary', { container: scanningBoundaryPoint.startContainer, current: walker.currentNode }); 175 | break; 176 | } 177 | 178 | if (walker.currentNode.nodeType == Node.TEXT_NODE) { 179 | range.setStart(walker.currentNode, 0); 180 | } 181 | else { 182 | range.setStartBefore(walker.currentNode); 183 | } 184 | const { text: partialText, state } = nodeToText(walker.currentNode); 185 | switch (state) { 186 | case STATE_CONTINUE_PHYSICALLY: 187 | text = `${partialText}${text}`; 188 | continue; 189 | 190 | case STATE_CONTINUE_VISUALLY: 191 | texts.unshift(text); 192 | ranges.unshift(range.cloneRange()); 193 | boundaryInlineNodes.unshift(walker.currentNode.offsetParent); 194 | text = partialText; 195 | range.collapse(true); 196 | continue; 197 | 198 | case STATE_SEPARATED: 199 | break; 200 | } 201 | } 202 | texts.unshift(text); 203 | ranges.unshift(range); 204 | 205 | scanningBoundaryPoint.detach(); 206 | scanningRange.detach(); 207 | 208 | return { texts, ranges, boundaryInlineNodes }; 209 | } 210 | 211 | function getFollowingRanges(sourceRange) { 212 | const texts = []; 213 | const ranges = []; 214 | const boundaryInlineNodes = []; 215 | const range = document.createRange(); 216 | range.setStart(sourceRange.endContainer, sourceRange.endOffset); 217 | range.setEnd(sourceRange.endContainer, sourceRange.endOffset); 218 | 219 | const scanningBoundaryPoint = range.cloneRange(); 220 | scanningBoundaryPoint.selectNodeContents(findNearestContainerElement(sourceRange.endContainer)); 221 | scanningBoundaryPoint.collapse(false); 222 | const scanningRange = range.cloneRange(); 223 | 224 | const walker = createVisibleTextNodeWalker(); 225 | walker.currentNode = range.endContainer; 226 | let text = ''; 227 | if (walker.currentNode.nodeType == Node.TEXT_NODE) { 228 | text += walker.currentNode.nodeValue.substring(range.endOffset); 229 | } 230 | else { 231 | const nextNode = walker.currentNode.childNodes[range.endOffset]; 232 | if (nextNode) 233 | walker.currentNode = nextNode; 234 | } 235 | while (walker.nextNode()) { 236 | // We should accept text nodes in the same container. 237 | scanningRange.selectNode(walker.currentNode); 238 | if (scanningRange.compareBoundaryPoints(Range.END_TO_START, scanningBoundaryPoint) >= 0) { 239 | //console.log('getFollowingRanges: reached to the boundary', { container: scanningBoundaryPoint.endContainer, current: walker.currentNode }); 240 | break; 241 | } 242 | 243 | if (walker.currentNode.nodeType == Node.TEXT_NODE) { 244 | range.setEnd(walker.currentNode, walker.currentNode.nodeValue.length); 245 | } 246 | else { 247 | range.setEndAfter(walker.currentNode); 248 | } 249 | const { text: partialText, state } = nodeToText(walker.currentNode); 250 | switch (state) { 251 | case STATE_CONTINUE_PHYSICALLY: 252 | text += partialText; 253 | continue; 254 | 255 | case STATE_CONTINUE_VISUALLY: 256 | texts.push(text); 257 | ranges.push(range.cloneRange()); 258 | boundaryInlineNodes.push(walker.currentNode.offsetParent); 259 | text = partialText; 260 | range.collapse(false); 261 | continue; 262 | 263 | case STATE_SEPARATED: 264 | break; 265 | } 266 | } 267 | texts.push(text); 268 | ranges.push(range); 269 | 270 | scanningBoundaryPoint.detach(); 271 | scanningRange.detach(); 272 | 273 | return { texts, ranges, boundaryInlineNodes }; 274 | } 275 | 276 | let nodeVisibilityCache; 277 | function clearNodeVisibilityCache() { 278 | nodeVisibilityCache = null; 279 | } 280 | 281 | function createVisibleTextNodeWalker() { 282 | return document.createTreeWalker( 283 | document, 284 | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, 285 | { acceptNode: (node) => 286 | isNodeVisible(node) ? 287 | NodeFilter.FILTER_ACCEPT : 288 | NodeFilter.FILTER_REJECT }, 289 | false 290 | ); 291 | } 292 | 293 | function isNodeVisible(node) { 294 | if (node.nodeType == Node.TEXT_NODE) 295 | node = node.parentNode; 296 | 297 | if (!nodeVisibilityCache) 298 | nodeVisibilityCache = new WeakMap(); 299 | if (nodeVisibilityCache.has(node)) 300 | return nodeVisibilityCache.get(node); 301 | 302 | // We should return earlier if the node is certainly invisible, 303 | // because getClientRects and elementsFromPointElements are slow. 304 | if (node.offsetWidth == 0 || node.offsetHeight == 0) { 305 | nodeVisibilityCache.set(node, false); 306 | return false; 307 | } 308 | 309 | const rects = node.getClientRects(); 310 | if (rects.length == 0) { 311 | nodeVisibilityCache.set(node, false); 312 | return false; 313 | } 314 | 315 | const rect = rects[0]; 316 | // elementsFromPointElements doesn't list "visibility: hidden" elements, 317 | // so we don't need to do double-check with computed style. 318 | const visibleElements = document.elementsFromPoint(rect.left, rect.top); 319 | const hit = new Set(visibleElements).has(node); 320 | nodeVisibilityCache.set(node, hit); 321 | return hit; 322 | } 323 | 324 | 325 | // returns rangeData compatible object 326 | // See also: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/find/find 327 | function getRangeData(range) { 328 | let startContainer = range.startContainer; 329 | let startOffset = range.startOffset; 330 | let endContainer = range.endContainer; 331 | let endOffset = range.endOffset; 332 | if (startContainer.nodeType != Node.TEXT_NODE) { 333 | const possibleStartContainer = startContainer.childNodes[startOffset]; 334 | startContainer = evaluateXPath( 335 | `self::text() || following::text()[1]`, 336 | possibleStartContainer, 337 | XPathResult.FIRST_ORDERED_NODE_TYPE 338 | ).singleNodeValue || possibleStartContainer; 339 | startOffset = 0; 340 | } 341 | if (endContainer.nodeType != Node.TEXT_NODE) { 342 | let possibleEndContainer = endContainer.childNodes[Math.max(0, endOffset - 1)]; 343 | if (possibleEndContainer.nodeType != Node.TEXT_NODE) { 344 | const walker = document.createTreeWalker(document, NodeFilter.SHOW_TEXT, null, false); 345 | walker.currentNode = possibleEndContainer; 346 | possibleEndContainer = walker.previousNode(); 347 | } 348 | endContainer = possibleEndContainer; 349 | endOffset = endContainer.nodeValue.length; 350 | } 351 | return { 352 | startTextNodePos: getTextNodePosition(startContainer), 353 | startOffset: startOffset, 354 | endTextNodePos: getTextNodePosition(endContainer), 355 | endOffset: endOffset 356 | }; 357 | } 358 | 359 | function getFieldRangeData(field) { 360 | return { 361 | text: field.value, 362 | startOffset: field.selectionStart, 363 | endOffset: field.selectionEnd 364 | }; 365 | } 366 | 367 | function selectRanges(ranges) { 368 | if (!Array.isArray(ranges)) 369 | ranges = [ranges]; 370 | 371 | if (ranges.length == 0) 372 | return; 373 | 374 | gChangingSelectionRangeInternally++; 375 | setTimeout(() => { 376 | gChangingSelectionRangeInternally--; 377 | }, 100); 378 | 379 | if ('fieldNodePos' in ranges[0]) { 380 | // fake, text ranges 381 | const field = getFieldNodeAt(ranges[0].fieldNodePos); 382 | if (!field) 383 | return; 384 | field.setSelectionRange( 385 | ranges[0].startOffset, 386 | ranges[ranges.length - 1].endOffset 387 | ); 388 | field.focus(); 389 | return; 390 | } 391 | 392 | // ranges 393 | const selection = window.getSelection(); 394 | selection.removeAllRanges(); 395 | for (let range of ranges) { 396 | range = createRangeFromRangeData(range); 397 | selection.addRange(range); 398 | } 399 | } 400 | 401 | function getTextNodePosition(node) { 402 | return evaluateXPath( 403 | 'count(preceding::text())', 404 | node, 405 | XPathResult.NUMBER_TYPE 406 | ).numberValue; 407 | } 408 | 409 | function findNearestContainerElement(node) { 410 | let container = node; 411 | if (container.nodeType != Node.ELEMENT_NODE) 412 | container = container.parentNode; 413 | while (container && 414 | /^inline/.test(window.getComputedStyle(container, null).display)) { 415 | container = container.parentNode; 416 | } 417 | 418 | if (node.offsetParent && 419 | container.contains(node.offsetParent)) 420 | return node.offsetParent; 421 | 422 | return container; 423 | } 424 | 425 | const kINPUT_TEXT_CONDITION = `${toLowerCase('local-name()')} = "input" and ${toLowerCase('@type')} = "text"`; 426 | const kTEXT_AREA_CONDITION = `${toLowerCase('local-name()')} = "textarea"`; 427 | var kFIELD_CONDITION = `(${kINPUT_TEXT_CONDITION}) or (${kTEXT_AREA_CONDITION})`; 428 | 429 | function getFieldNodePosition(node) { 430 | return evaluateXPath( 431 | `count(preceding::*[${kFIELD_CONDITION}])`, 432 | node, 433 | XPathResult.NUMBER_TYPE 434 | ).numberValue; 435 | } 436 | 437 | function createRangeFromRangeData(data) { 438 | const range = document.createRange(); 439 | range.setStart(getTextNodeAt(data.startTextNodePos), data.startOffset); 440 | range.setEnd(getTextNodeAt(data.endTextNodePos), data.endOffset); 441 | return range; 442 | } 443 | 444 | function getTextNodeAt(position) { 445 | return evaluateXPath( 446 | `descendant::text()[position()=${position+1}]`, 447 | document, 448 | XPathResult.FIRST_ORDERED_NODE_TYPE 449 | ).singleNodeValue; 450 | } 451 | 452 | function getFieldNodeAt(position) { 453 | return evaluateXPath( 454 | `descendant::*[${kFIELD_CONDITION}][position()=${position+1}]`, 455 | document, 456 | XPathResult.FIRST_ORDERED_NODE_TYPE 457 | ).singleNodeValue; 458 | } 459 | -------------------------------------------------------------------------------- /licenses/MPL2.0.txt: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /webextensions/background/uriMatcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 'use strict'; 7 | 8 | var URIMatcher = { 9 | matchSingle: async function(params) { 10 | log('matchSingle: ', params); 11 | try{ 12 | await this.initialized; 13 | this._updateURIRegExp(); 14 | const match = this.matchMaybeURIs(params.text); 15 | if (match.length == 0) 16 | return null; 17 | 18 | const longestResultAt = new Map(); 19 | for (let maybeURI of match) { 20 | maybeURI = this.sanitizeURIString(maybeURI); 21 | const uriRange = await this.findTextRange({ 22 | text: maybeURI, 23 | range: params.cursor, 24 | tabId: params.tabId 25 | }); 26 | if (!uriRange) { 27 | continue; 28 | } 29 | const positionKey = `${uriRange.startTextNodePos}:${uriRange.startOffset}`; 30 | const result = { 31 | text: maybeURI, 32 | range: uriRange, 33 | uri: this.fixupURI(maybeURI, params.baseURI) 34 | }; 35 | const longestResult = longestResultAt.get(positionKey); 36 | if (!longestResult || 37 | (longestResult.text.length <= maybeURI.length && 38 | maybeURI.startsWith(longestResult.text))) { 39 | longestResultAt.set(positionKey, result); 40 | } 41 | } 42 | if (longestResultAt.size > 0) { 43 | return [...longestResultAt.values()][0]; 44 | } 45 | log(' => no match'); 46 | } 47 | catch(error){ 48 | console.error(error); 49 | } 50 | return null; 51 | }, 52 | 53 | matchAll: async function(params) { 54 | log('matchAll: ', params); 55 | const results = new Set(); 56 | try{ 57 | await this.initialized; 58 | params.onProgress && params.onProgress(0); 59 | this._updateURIRegExp(); 60 | const startAt = Date.now(); 61 | 62 | let maxCount = 0; 63 | const uniqueURIs = {}; 64 | for (const range of params.ranges) { 65 | const match = this.matchMaybeURIs(range.expandedText || range.text); 66 | if (match.length == 0) { 67 | range.maybeURIs = []; 68 | continue; 69 | } 70 | log('matchAll: loop for a range: ', { range, match }); 71 | 72 | const maybeURIs = Array.from(match).map(maybeURI => this.sanitizeURIString(maybeURI)); 73 | log('matchAll: maybeURIs: ', maybeURIs); 74 | range.maybeURIs = []; 75 | for (const maybeURI of maybeURIs) { 76 | const uri = this.fixupURI(maybeURI, params.baseURI); 77 | if (uri in uniqueURIs) 78 | continue; 79 | uniqueURIs[uri] = true; 80 | range.maybeURIs.push({ 81 | original: maybeURI, 82 | uri: uri 83 | }); 84 | } 85 | log('matchAll: range.maybeURIs: ', range.maybeURIs); 86 | maxCount += range.maybeURIs.length; 87 | } 88 | 89 | let count = 0; 90 | const longestResultAt = new Map(); 91 | for (const range of params.ranges) { 92 | for (const maybeURI of range.maybeURIs) { 93 | const uriRange = await this.findTextRange({ 94 | text: maybeURI.original, 95 | range: range, 96 | tabId: params.tabId 97 | }); 98 | log('matchAll: uriRange for URI: ', maybeURI.original, uriRange); 99 | if (uriRange) { 100 | // If the find range contains multiple URIs and longer variations 101 | // appear after its shorter versions, shorter versions may be 102 | // detected wrongly as "found reuslt" from the selection range. 103 | // For example, on a case like: 104 | // http://example.com/ , http://example.com/foo/ 105 | // both "http://example.com/" and "http://example.com/foo" will be 106 | // listed as found results. 107 | // To avoid such an unexpected result, we need to reject all 108 | // shorter versions starting from the same position. 109 | const positionKey = `${range.startTextNodePos}:${range.startOffset}`; 110 | const longestResult = longestResultAt.get(positionKey); 111 | if (longestResult && 112 | longestResult.text.length <= maybeURI.original.length && 113 | maybeURI.original.startsWith(longestResult.text)) { 114 | results.delete(longestResult); 115 | } 116 | const result = { 117 | text: maybeURI.original, 118 | range: uriRange, 119 | uri: maybeURI.uri 120 | }; 121 | longestResultAt.set(positionKey, result); 122 | results.add(result); 123 | } 124 | count++; 125 | if (Date.now() - startAt > 250) 126 | params.onProgress && params.onProgress(count / maxCount); 127 | if (count % 100 == 0) 128 | await wait(0); 129 | } 130 | } 131 | params.onProgress && params.onProgress(1); 132 | const sortedResults = [...results].sort((aA, aB) => 133 | aA.range.startTextNodePos - aB.range.startTextNodePos || 134 | aA.range.startOffset - aB.range.startOffset); 135 | log(' => ', sortedResults); 136 | return sortedResults; 137 | } 138 | catch(error){ 139 | console.error(error); 140 | log(' => no result'); 141 | } 142 | return []; 143 | }, 144 | 145 | matchMaybeURIs(text) { 146 | const start = Date.now(); 147 | let match = text.match(this._URIMatchingRegExp); 148 | log('matchMaybeURIs matching: ', Date.now() - start, 'msec for ', text.length, ' characters text, ', this._URIMatchingRegExp.source.length, ' characters regexp'); 149 | if (!match) 150 | return []; 151 | match = [...match].filter(maybeURI => ( 152 | (!this.hasLoadableScheme(maybeURI) && 153 | !this.URIExceptionPattern_all.test(maybeURI)) || 154 | this.isHeadOfNewURI(maybeURI) 155 | )); 156 | if (match.length == 0) 157 | return []; 158 | return match; 159 | }, 160 | 161 | findTextRange: async function(params) { 162 | if (params.text.length <= 0) 163 | return null; 164 | 165 | if (!('startTextNodePos' in params.range) || 166 | !('endTextNodePos' in params.range)) { 167 | // text, fake range 168 | const wholeText = params.range.text; 169 | const length = params.text.length; 170 | let startAt = 0; 171 | while (true) { 172 | const index = wholeText.indexOf(params.text, startAt); 173 | startAt = index + length; 174 | if (index < 0) 175 | return null; 176 | if (index > params.range.endOffset || 177 | index + length < params.range.startOffset) 178 | continue; 179 | return { 180 | startOffset: index, 181 | endOffset: index + length, 182 | text: params.text 183 | }; 184 | } 185 | return null; 186 | } 187 | 188 | // get real range 189 | let match; 190 | const startTryAt = Date.now(); 191 | while (true) { 192 | match = await browser.find.find(params.text, { 193 | tabId: params.tabId, 194 | caseSensitive: true, 195 | includeRangeData: true 196 | }); 197 | if (match.rangeData && 198 | match.rangeData.length > 0) 199 | break; 200 | 201 | // Clicked URI text may not be found if the webpage contents are 202 | // modified while we are processing. So we need to retry for a while. 203 | // See also: https://github.com/piroor/textlink/issues/66 204 | if (Date.now() - startTryAt > this.configs.rangeFindTimeout) 205 | throw new Error('FATAL ERROR: Page contents are modified while finding clicked URI text!'); 206 | 207 | await new Promise(resolve => setTimeout(resolve, this.configs.rangeFindRetryDelay)); 208 | } 209 | 210 | for (const rangeData of match.rangeData) { 211 | if (rangeData.framePos != params.range.framePos || 212 | rangeData.startTextNodePos > params.range.endTextNodePos || 213 | (rangeData.startTextNodePos == params.range.endTextNodePos && 214 | rangeData.startOffset > params.range.endOffset) || 215 | rangeData.endTextNodePos < params.range.startTextNodePos || 216 | (rangeData.endTextNodePos == params.range.startTextNodePos && 217 | rangeData.endOffset < params.range.startOffset)) 218 | continue; 219 | return rangeData; 220 | } 221 | return null; 222 | }, 223 | 224 | kDomainSeparators : '\\.', 225 | kMultibyteDomainSeparators : '\\.\uff0e', 226 | kIDNDomainSeparators : '\\.\u3002\uff0e', 227 | 228 | // Forbidden characters in IDN are defined by RFC 3491. 229 | // http://www.ietf.org/rfc/rfc3491.txt 230 | // http://www5d.biglobe.ne.jp/~stssk/rfc/rfc3491j.html 231 | // and 232 | // http://www.ietf.org/rfc/rfc3454.txt 233 | // http://www.jdna.jp/survey/rfc/rfc3454j.html 234 | kStringprepForbiddenCharacters : '\\u0000-\\u0020\\u0080-\\u00A0\\u0340\\u0341\\u06DD\\u070F\\u1680\\u180E\\u2000-\\u200F\\u2028-\\u202F\\u205F-\\u2063\\u206A-\\u206F\\u2FF0-\\u2FFB\\u3000\\uD800-\\uF8FF\\uFDD0-\\uFDEF\\uFEFF\\uFFF9-\\uFFFF', 235 | kStringprepReplaceToNothingRegExp : /[\u00AD\u034F\u1806\u180B-\u180D\u200B-\u200D\u2060\uFE00-\uFE0F\uFEFF]/g, 236 | 237 | URIPatterns_base : '\\(?(%URI_PATTERN%)', 238 | URIPatternsMultibyte_base : '[\\(\uff08]?(%URI_PATTERN%)', 239 | 240 | URIPattern_base : '%SCHEME_PATTERN%(?://)?%LOGIN_PATTERN%%POSSIBLE_DOMAIN_PATTERN%(?:/(?:%PART_PATTERN%)?)?|%LOGIN_PATTERN%%DOMAIN_PATTERN%(?:/%PART_PATTERN%)?', 241 | URIPatternRelative_base : '%PART_PATTERN%(?:\\.|/)%PART_PATTERN%', 242 | 243 | URIPatternMultibyte_base : '%SCHEME_PATTERN%(?://|\uff0f\uff0f)?%LOGIN_PATTERN%%POSSIBLE_DOMAIN_PATTERN%(?:[/\uff0f](?:%PART_PATTERN%)?)?|%LOGIN_PATTERN%%DOMAIN_PATTERN%(?:[/\uff0f](?:%PART_PATTERN%)?)?', 244 | URIPatternMultibyteRelative_base : '%PART_PATTERN%[\\.\uff0e/\uff0f]%PART_PATTERN%', 245 | 246 | kSchemePattern : '[\\*\\+a-z0-9_]+:', 247 | kSchemePatternMultibyte : '[\\*\\+a-z0-9_\uff41-\uff5a\uff21-\uff3a\uff10-\uff19\uff3f]+[:\uff1a]', 248 | 249 | kLoginPattern : '(?:[^\\s\u3000/:]+(?::[^\\s\u3000/@]+)?@)?', 250 | kLoginPatternMultibyte : '(?:[^\\s\u3000/:\uff0f\uff1a]+(?:[:\uff1a][^\\s\u3000@/\uff0f\uff20]+)?[@\uff20])?', 251 | 252 | kURIPattern_part : '[-_\\.!~*\'()a-z0-9;/?:@&=+$,%#]+', 253 | kURIPatternMultibyte_part : '[-_\\.!~*\'()a-z0-9;/?:@&=+$,%#\u301c\uff0d\uff3f\uff0e\uff01\uff5e\uffe3\uff0a\u2019\uff08\uff09\uff41-\uff5a\uff21-\uff3a\uff10-\uff19\uff1b\uff0f\uff1f\uff1a\uff20\uff06\uff1d\uff0b\uff04\uff0c\uff05\uff03]+', 254 | 255 | _parenPatterns : [ 256 | /^["\u201d\u201c\u301d\u301f](.+)["\u201d\u201c\u301d\u301f]$/, 257 | /^[`'\u2019\u2018](.+)[`'\u2019\u2018]$/, 258 | /^[(\uff08](.+)[)\uff09]$/, 259 | /^[{\uff5b](.+)[}\uff5d]$/, 260 | /^[\[\uff3b](.+)[\]\uff3d]$/, 261 | /^[<\uff1c](.+)[>\uff1e]$/, 262 | /^[\uff62\u300c](.+)[\uff63\u300d]$/, 263 | /^\u226a(.+)\u226b$/, 264 | /^\u3008(.+)\u3009$/, 265 | /^\u300a(.+)\u300b$/, 266 | /^\u300e(.+)\u300f$/, 267 | /^\u3010(.+)\u3011$/, 268 | /^\u3014(.+)\u3015$/, 269 | /^(.+)["\u201d\u201c\u301d\u301f][^"\u201d\u201c\u301d\u301f]*$/, 270 | /^(.+)[`'\u2019\u2018][^`'\u2019\u2018]*$/, 271 | /^(.+)[(\uff08][^)\uff09]*$/, 272 | /^(.+)[{\uff5b][^}\uff5d]*$/, 273 | /^(.+)[\[\uff3b][^\]\uff3d]*$/, 274 | /^(.+)[<\uff1c][^>\uff1e]*$/, 275 | /^(.+)[\uff62\u300c][^\uff63\u300d]*$/, 276 | /^(.+)\u226a[^\u226b$]*/, 277 | /^(.+)\u3008[^\u3009$]*/, 278 | /^(.+)\u300a[^\u300b$]*/, 279 | /^(.+)\u300e[^\u300f$]*/, 280 | /^(.+)\u3010[^\u3011$]*/, 281 | /^(.+)\u3014[^\u3015$]*/ 282 | ], 283 | 284 | get scheme() { 285 | return this._scheme; 286 | }, 287 | set scheme(value) { 288 | this._scheme = value; 289 | this._schemes = this.niceSplit(this.expandWildcardsToRegExp(this.scheme)); 290 | this.IDNScheme = this.IDNScheme; // reset IDN-enabled schemes list 291 | this.invalidatePatterns(); 292 | return value; 293 | }, 294 | _scheme : '', 295 | get schemes() { 296 | return this._schemes.concat(this._fixupSchemes).sort(); 297 | }, 298 | _schemes : [], 299 | _fixupSchemes : [], 300 | 301 | set IDNScheme(value) { 302 | this._IDNScheme = value; 303 | this._IDNSchemes = this.niceSplit(this.expandWildcardsToRegExp(this._IDNScheme)) 304 | .filter(function(scheme) { 305 | return this.schemes.indexOf(scheme) > -1; 306 | }, this); 307 | this.invalidatePatterns(); 308 | return value; 309 | }, 310 | get IDNScheme() { 311 | return this._IDNScheme; 312 | }, 313 | _IDNScheme : '', 314 | get IDNSchemes() { 315 | if (this.configs.IDNEnabled) { 316 | if (this._fixupIDNSchemes === null) { 317 | this._fixupIDNSchemes = []; 318 | for (const i in this._fixupTargetsHash) { 319 | if (!this._fixupTargetsHash.hasOwnProperty(i)) 320 | continue; 321 | const fixUpToMatch = this._fixupTargetsHash[i].match(/^(\w+):/); 322 | const fixUpFromMatch = i.match(/^(\w+):/); 323 | if (fixUpToMatch && 324 | this._IDNSchemes.indexOf(fixUpToMatch[1]) > -1 && 325 | fixUpFromMatch) 326 | this._fixupIDNSchemes.push(fixUpFromMatch[1]); 327 | } 328 | } 329 | return this._IDNSchemes.concat(this._fixupIDNSchemes).sort(); 330 | } 331 | else { 332 | return []; 333 | } 334 | }, 335 | _IDNSchemes : [], 336 | _fixupIDNSchemes : null, 337 | 338 | get nonIDNSchemes() { 339 | if (this.configs.IDNEnabled) { 340 | if (this._nonIDNSchemes === null) { 341 | const IDNSchemes = this.IDNSchemes; 342 | this._nonIDNSchemes = this.schemes 343 | .filter(scheme => IDNSchemes.indexOf(scheme) < 0); 344 | } 345 | return this._nonIDNSchemes; 346 | } 347 | else { 348 | return this.schemes; 349 | } 350 | }, 351 | _nonIDNSchemes : null, 352 | 353 | get schemeFixupTable() { 354 | return this._schemeFixupTable; 355 | }, 356 | set schemeFixupTable(value) { 357 | this._schemeFixupTable = value; 358 | 359 | this._fixupTable = this._schemeFixupTable 360 | .replace(/(\s*[^:,\s]+)\s*=>\s*([^:,\s]+)(\s*([,\| \n\r\t]|$))/g, '$1:=>$2:$3'); 361 | 362 | this._fixupTargets = []; 363 | this._fixupTargetsHash = {}; 364 | this._fixupSchemes = []; 365 | this.niceSplit(this.expandWildcardsToRegExp(this._fixupTable)) 366 | .forEach(target => { 367 | const [fixUpFrom, fixUpTo] = target.split(/\s*=>\s*/); 368 | if (!fixUpFrom || !fixUpTo) 369 | return; 370 | this._fixupTargetsHash[fixUpFrom] = fixUpTo; 371 | this._fixupTargets.push(fixUpFrom); 372 | const match = fixUpFrom.match(/^(\w+):/); 373 | if (match) 374 | this._fixupSchemes.push(match[1]); 375 | }); 376 | 377 | this._fixupTargets.sort().forEach(target => { 378 | this._fixupTargetsHash[target] = this._fixupTargetsHash[target]; 379 | }); 380 | this._fixupSchemes.sort(); 381 | 382 | this._fixupTargetsPattern = this._fixupTargets.join('|'); 383 | this._fixupTargetsRegExp = new RegExp(`^(${this._fixupTargetsPattern})`); 384 | 385 | this.invalidatePatterns(); 386 | return value; 387 | }, 388 | _schemeFixupTable : '', 389 | 390 | 391 | // regexp 392 | 393 | get URIPattern() { 394 | if (!this._URIPattern) { 395 | const patterns = []; 396 | const base = this.URIPattern_base 397 | .replace( 398 | /%PART_PATTERN%/g, 399 | this.URIPattern_part 400 | ) 401 | .replace( 402 | /%LOGIN_PATTERN%/g, 403 | this.kLoginPattern 404 | ); 405 | patterns.push(base 406 | .replace( 407 | /%SCHEME_PATTERN%/g, 408 | `(?:${this.nonIDNSchemes.join('|')}):` 409 | ) 410 | .replace( 411 | /%POSSIBLE_DOMAIN_PATTERN%/g, 412 | this.getDomainPattern(kDOMAIN_LAZY) 413 | ) 414 | .replace( 415 | /%DOMAIN_PATTERN%/g, 416 | this.getDomainPattern() 417 | )); 418 | if (this.configs.IDNEnabled) 419 | patterns.push(base 420 | .replace( 421 | /%SCHEME_PATTERN%/g, 422 | `(?:${this.IDNSchemes.join('|')}):` 423 | ) 424 | .replace( 425 | /%POSSIBLE_DOMAIN_PATTERN%/g, 426 | this.getDomainPattern(kDOMAIN_LAZY | kDOMAIN_IDN) 427 | ) 428 | .replace( 429 | /%DOMAIN_PATTERN%/g, 430 | this.getDomainPattern(kDOMAIN_IDN) 431 | )); 432 | this._URIPattern = this.URIPatterns_base.replace(/%URI_PATTERN%/g, patterns.join('|')); 433 | } 434 | 435 | return this._URIPattern; 436 | }, 437 | _URIPattern : null, 438 | 439 | get URIPatternRelative() { 440 | if (!this._URIPatternRelative) { 441 | this._URIPatternRelative = this.URIPatternRelative_base 442 | .replace( 443 | /%PART_PATTERN%/g, 444 | this.URIPattern_part 445 | ); 446 | } 447 | 448 | return this._URIPatternRelative; 449 | }, 450 | _URIPatternRelative : null, 451 | 452 | get URIPatternMultibyte() { 453 | if (!this._URIPatternMultibyte) { 454 | const patterns = []; 455 | const base = this.URIPatternMultibyte_base 456 | .replace( 457 | /%PART_PATTERN%/g, 458 | this.URIPatternMultibyte_part 459 | ) 460 | .replace( 461 | /%LOGIN_PATTERN%/g, 462 | this.kLoginPatternMultibyte 463 | ); 464 | patterns.push(base 465 | .replace( 466 | /%SCHEME_PATTERN%/g, 467 | '(?:'+ 468 | this.nonIDNSchemes.map(scheme => { 469 | return `${scheme}|${this.convertHalfWidthToFullWidth(scheme)}`; 470 | }).join('|')+ 471 | ')[:\uff1a]' 472 | ) 473 | .replace( 474 | /%POSSIBLE_DOMAIN_PATTERN%/g, 475 | this.getDomainPattern(kDOMAIN_MULTIBYTE | kDOMAIN_LAZY) 476 | ) 477 | .replace( 478 | /%DOMAIN_PATTERN%/g, 479 | this.getDomainPattern(kDOMAIN_MULTIBYTE) 480 | )); 481 | if (this.configs.IDNEnabled) 482 | patterns.push(base 483 | .replace( 484 | /%SCHEME_PATTERN%/g, 485 | '(?:'+ 486 | this.IDNSchemes.map(scheme => { 487 | return `${scheme}|${this.convertHalfWidthToFullWidth(scheme)}`; 488 | }).join('|')+ 489 | ')[:\uff1a]' 490 | ) 491 | .replace( 492 | /%POSSIBLE_DOMAIN_PATTERN%/g, 493 | this.getDomainPattern(kDOMAIN_MULTIBYTE | kDOMAIN_LAZY | kDOMAIN_IDN) 494 | ) 495 | .replace( 496 | /%DOMAIN_PATTERN%/g, 497 | this.getDomainPattern(kDOMAIN_MULTIBYTE | kDOMAIN_IDN) 498 | )); 499 | this._URIPatternMultibyte = this.URIPatternsMultibyte_base.replace(/%URI_PATTERN%/g, patterns.join('|')); 500 | } 501 | 502 | return this._URIPatternMultibyte; 503 | }, 504 | _URIPatternMultibyte : null, 505 | 506 | get URIPatternMultibyteRelative() { 507 | if (!this._URIPatternMultibyteRelative) { 508 | this._URIPatternMultibyteRelative = this.URIPatternMultibyteRelative_base 509 | .replace( 510 | /%PART_PATTERN%/g, 511 | this.URIPatternMultibyte_part 512 | ); 513 | } 514 | 515 | return this._URIPatternMultibyteRelative; 516 | }, 517 | _URIPatternMultibyteRelative : null, 518 | 519 | getDomainPattern(optionsFlag) { 520 | optionsFlag = optionsFlag || 0; 521 | let pattern = this._domainPatterns[optionsFlag]; 522 | if (!pattern) { 523 | if (optionsFlag & kDOMAIN_IDN) { 524 | let forbiddenCharacters = this.kStringprepForbiddenCharacters+ 525 | this.kIDNDomainSeparators+ 526 | ':/@\uff1a\uff0f\uff20'; 527 | if (!(optionsFlag & kDOMAIN_LAZY)) 528 | forbiddenCharacters += this.configs.IDNLazyDetectionSeparators; 529 | const part = '[^'+ 530 | forbiddenCharacters+ 531 | (this.configs.IDNBlacklistChars || '') 532 | .replace(new RegExp(`[${forbiddenCharacters}]`, 'g'), '') 533 | .replace(/(.)\1+/g, '$1') 534 | .replace(/./g, function(aChar) { 535 | const code = `00${aChar.charCodeAt(0).toString(16)}`.substr(-4, 4); 536 | return `\\u${code}`; 537 | })+ 538 | ']+'; 539 | pattern = `${part}(?:[${this.kIDNDomainSeparators}]${part})*`; 540 | } 541 | else if (optionsFlag & kDOMAIN_MULTIBYTE) { 542 | const part = '[0-9a-z-\uff10-\uff19\uff41-\uff5a\uff21-\uff3a\uff0d]+'; 543 | pattern = `${part}(?:[${this.kMultibyteDomainSeparators}]${part})*`; 544 | } 545 | else { 546 | const part = '[0-9a-z-]+'; 547 | pattern = `${part}(?:${this.kDomainSeparators + part})*`; 548 | } 549 | 550 | if (!(optionsFlag & kDOMAIN_LAZY) || 551 | optionsFlag & kDOMAIN_IDN) 552 | pattern += this.getTLDPattern(optionsFlag); 553 | 554 | if (optionsFlag & kDOMAIN_IDN || 555 | optionsFlag & kDOMAIN_MULTIBYTE) { 556 | pattern += '(?:[:\uff1a][0-9\uff10-\uff19]+)?'; 557 | } 558 | else { 559 | pattern += '(?::[0-9]+)?'; 560 | } 561 | 562 | this._domainPatterns[optionsFlag] = pattern; 563 | } 564 | return pattern; 565 | }, 566 | _domainPatterns : {}, 567 | 568 | getTLDPattern(optionsFlag) { 569 | const TLD = this.topLevelDomains; 570 | const halfWidthTLDPattern = `(?:${TLD.join('|')})\\b`; 571 | const TLDPattern = optionsFlag & kDOMAIN_MULTIBYTE || optionsFlag & kDOMAIN_IDN ? 572 | `(?:${ 573 | [halfWidthTLDPattern] 574 | .concat(TLD.map(this.convertHalfWidthToFullWidth, this)) 575 | .join('|') 576 | })` : 577 | halfWidthTLDPattern ; 578 | return (optionsFlag & kDOMAIN_IDN ? 579 | `[${this.kIDNDomainSeparators}]` : 580 | optionsFlag & kDOMAIN_MULTIBYTE ? 581 | `[${this.kMultibyteDomainSeparators}]` : 582 | this.kDomainSeparators 583 | ) + TLDPattern; 584 | }, 585 | 586 | get URIPattern_part() { 587 | if (!this._URIPattern_part) { 588 | this._URIPattern_part = this.configs.i18nPathEnabled ? 589 | `[^${this.kStringprepForbiddenCharacters}]+` : 590 | this.kURIPattern_part ; 591 | } 592 | return this._URIPattern_part; 593 | }, 594 | _URIPattern_part : null, 595 | get URIPatternMultibyte_part() { 596 | if (!this._URIPatternMultibyte_part) { 597 | this._URIPatternMultibyte_part = this.configs.i18nPathEnabled ? 598 | `[^${this.kStringprepForbiddenCharacters}]+` : 599 | this.kURIPatternMultibyte_part ; 600 | } 601 | return this._URIPatternMultibyte_part; 602 | }, 603 | _URIPatternMultibyte_part : null, 604 | 605 | get findURIPatternPart() { 606 | if (!this._findURIPatternPart) { 607 | this._findURIPatternPart = this.configs.i18nPathEnabled || this.configs.IDNEnabled ? 608 | `[^${this.kStringprepForbiddenCharacters}]+` : 609 | this.kURIPattern_part ; 610 | } 611 | return this._findURIPatternPart; 612 | }, 613 | _findURIPatternPart : null, 614 | get findURIPatternMultibytePart() { 615 | if (!this._findURIPatternMultibytePart) { 616 | this._findURIPatternMultibytePart = this.configs.i18nPathEnabled || this.configs.IDNEnabled ? 617 | `[^${this.kStringprepForbiddenCharacters}]+` : 618 | this.kURIPatternMultibyte_part ; 619 | } 620 | return this._findURIPatternMultibytePart; 621 | }, 622 | _findURIPatternMultibytePart : null, 623 | 624 | get topLevelDomains() { 625 | if (!this._topLevelDomains) { 626 | let TLD = this.allTLD; 627 | if (!this.configs.IDNEnabled) 628 | TLD = TLD.filter(tld => /^[\.a-z]$/i.test(tld)); 629 | this._topLevelDomains = this.cleanUpArray(TLD.join(' ').replace(/^\s+|\s+$/g, '').split(/\s+/)) 630 | .reverse(); // this is required to match "com" instead of "co". 631 | } 632 | return this._topLevelDomains; 633 | }, 634 | _topLevelDomains : null, 635 | 636 | get URIExceptionPattern() { 637 | if (!this._URIExceptionPattern) 638 | this._updateURIExceptionPattern(); 639 | return this._URIExceptionPattern; 640 | }, 641 | get URIExceptionPattern_start() { 642 | if (!this._URIExceptionPattern_start) 643 | this._updateURIExceptionPattern(); 644 | return this._URIExceptionPattern_start; 645 | }, 646 | get URIExceptionPattern_end() { 647 | if (!this._URIExceptionPattern_end) 648 | this._updateURIExceptionPattern(); 649 | return this._URIExceptionPattern_end; 650 | }, 651 | get URIExceptionPattern_all() { 652 | if (!this._URIExceptionPattern_all) 653 | this._updateURIExceptionPattern(); 654 | return this._URIExceptionPattern_all; 655 | }, 656 | _updateURIExceptionPattern() { 657 | try { 658 | const whole = `^(?:${this.configs.partExceptionWhole})$`; 659 | const start = `^(?:${this.configs.partExceptionStart})`; 660 | const end = `(?:${this.configs.partExceptionEnd})$`; 661 | this._URIExceptionPattern = new RegExp(whole, 'i'); 662 | this._URIExceptionPattern_start = new RegExp(start, 'i'); 663 | this._URIExceptionPattern_end = new RegExp(end, 'i'); 664 | this._URIExceptionPattern_all = new RegExp(`${whole}|${start}|${end}`, 'i'); 665 | } 666 | catch(e) { 667 | this._URIExceptionPattern = /[^\w\W]/; 668 | this._URIExceptionPattern_start = /[^\w\W]/; 669 | this._URIExceptionPattern_end = /[^\w\W]/; 670 | this._URIExceptionPattern_all = /[^\w\W]/; 671 | } 672 | }, 673 | _URIExceptionPattern : null, 674 | _URIExceptionPattern_start : null, 675 | _URIExceptionPattern_end : null, 676 | _URIExceptionPattern_all : null, 677 | 678 | invalidatePatterns() { 679 | this._schemeRegExp = null; 680 | this._fixupIDNSchemes = null; 681 | this._nonIDNSchemes = null; 682 | 683 | this._domainPatterns = {}; 684 | this._topLevelDomains = null; 685 | this._topLevelDomainsRegExp = null; 686 | 687 | this._findURIPatternPart = null; 688 | this._findURIPatternMultibytePart = null; 689 | 690 | this._URIPattern_part = null; 691 | this._URIPattern = null; 692 | this._URIPatternMultibyte_part = null; 693 | this._URIPatternMultibyte = null; 694 | 695 | this._URIMatchingRegExp = null; 696 | this._URIMatchingRegExp_fromHead = null; 697 | this._URIPartFinderRegExp_start = null; 698 | this._URIPartFinderRegExp_end = null; 699 | }, 700 | 701 | invalidateExceptionPatterns() { 702 | this._URIExceptionPattern = null; 703 | this._URIExceptionPattern_start = null; 704 | this._URIExceptionPattern_end = null; 705 | this._URIExceptionPattern_all = null; 706 | }, 707 | 708 | cleanUpArray(aArray) { 709 | return aArray.slice(0) 710 | .sort() 711 | .join('\n') 712 | .replace(/^(.+)$\n(\1\n)+/gm, '$1\n') 713 | .split('\n'); 714 | }, 715 | 716 | // string operations 717 | 718 | // from http://taken.s101.xrea.com/blog/article.php?id=510 719 | convertFullWidthToHalfWidth(string) { 720 | return string.replace(this.fullWidthRegExp, this.f2h) 721 | .replace(/\u301c/g, '~'); // another version of tilde 722 | }, 723 | fullWidthRegExp : /[\uFF01\uFF02\uFF03\uFF04\uFF05\uFF06\uFF07\uFF08\uFF09\uFF0A\uFF0B\uFF0C\uFF0D\uFF0E\uFF0F\uFF10\uFF11\uFF12\uFF13\uFF14\uFF15\uFF16\uFF17\uFF18\uFF19\uFF1A\uFF1B\uFF1C\uFF1D\uFF1E\uFF1F\uFF20\uFF21\uFF22\uFF23\uFF24\uFF25\uFF26\uFF27\uFF28\uFF29\uFF2A\uFF2B\uFF2C\uFF2D\uFF2E\uFF2F\uFF30\uFF31\uFF32\uFF33\uFF34\uFF35\uFF36\uFF37\uFF38\uFF39\uFF3A\uFF3B\uFF3C\uFF3D\uFF3E\uFF3F\uFF40\uFF41\uFF42\uFF43\uFF44\uFF45\uFF46\uFF47\uFF48\uFF49\uFF4A\uFF4B\uFF4C\uFF4D\uFF4E\uFF4F\uFF50\uFF51\uFF52\uFF53\uFF54\uFF55\uFF56\uFF57\uFF58\uFF59\uFF5A\uFF5B\uFF5C\uFF5D\uFF5E]/g, 724 | f2h(aChar) { 725 | let code = aChar.charCodeAt(0); 726 | code &= 0x007F; 727 | code += 0x0020; 728 | return String.fromCharCode(code); 729 | }, 730 | 731 | convertHalfWidthToFullWidth(string) { 732 | return string.replace(this.halfWidthRegExp, this.h2f); 733 | }, 734 | halfWidthRegExp : /[!"#$%&'\(\)\*\+,-\.\/0123456789:;<=>\?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\[\\\]\^_`abcdefghijklmnopqrstuvwxyz\{\|\}~]/g, 735 | h2f(aChar) { 736 | let code = aChar.charCodeAt(0); 737 | code += 0xFF00; 738 | code -= 0x0020; 739 | return String.fromCharCode(code); 740 | }, 741 | 742 | expandWildcardsToRegExp(input) { 743 | return String(input) 744 | .replace(/([\(\)\+\.\{\}])/g, '\\$1') 745 | .replace(/\?/g, '.') 746 | .replace(/\*/g, '.+'); 747 | }, 748 | 749 | niceSplit(input) { 750 | return String(input) 751 | .split(/[\s\|,]+/) 752 | .filter(aItem => !!aItem); 753 | }, 754 | 755 | isHeadOfNewURI(string) { 756 | this._updateURIRegExp(); 757 | const start = Date.now(); 758 | let match = string.match(this._URIMatchingRegExp_fromHead); 759 | log('isHeadOfNewURI matching: ', Date.now() - start, 'msec for ', string.length, ' characters text, ', this._URIMatchingRegExp_fromHead.source.length, ' characters regexp'); 760 | match = match ? match[1] : '' ; 761 | return this.hasLoadableScheme(match) ? match == string : false ; 762 | }, 763 | _URIMatchingRegExp : null, 764 | _URIMatchingRegExp_fromHead : null, 765 | _updateURIRegExp() { 766 | if (this._URIMatchingRegExp) 767 | return; 768 | const regexp = []; 769 | if (this.configs.multibyteEnabled) { 770 | this._URIMatchingRegExp_fromHead = new RegExp(this.URIPatternMultibyte, 'i'); 771 | regexp.push(this.URIPatternMultibyte); 772 | if (this.configs.relativeEnabled) 773 | regexp.push(this.URIPatternMultibyteRelative); 774 | } 775 | else { 776 | this._URIMatchingRegExp_fromHead = new RegExp(this.URIPattern, 'i'); 777 | regexp.push(this.URIPattern); 778 | if (this.configs.relativeEnabled) 779 | regexp.push(this.URIPatternRelative); 780 | } 781 | this._URIMatchingRegExp = new RegExp(regexp.join('|'), 'ig'); 782 | }, 783 | 784 | getURIPartFromStart(string, excludeURIHead) { 785 | this._updateURIPartFinderRegExp(); 786 | const start = Date.now(); 787 | const match = string.match(this._URIPartFinderRegExp_start); 788 | log('getURIPartFromStart matching: ', Date.now() - start, 'msec for ', string.length, ' characters text, ', this._URIPartFinderRegExp_start.source.length, ' characters regexp'); 789 | const part = match ? match[1] : '' ; 790 | return (!excludeURIHead || !this.isHeadOfNewURI(part)) ? part : '' ; 791 | }, 792 | getURIPartFromEnd(string) { 793 | this._updateURIPartFinderRegExp(); 794 | const start = Date.now(); 795 | const match = string.match(this._URIPartFinderRegExp_end); 796 | log('getURIPartFromEnd matching: ', Date.now() - start, 'msec for ', string.length, ' characters text, ', this._URIPartFinderRegExp_end.source.length, ' characters regexp'); 797 | return match ? match[1] : '' ; 798 | }, 799 | _URIPartFinderRegExp_start : null, 800 | _URIPartFinderRegExp_end : null, 801 | _updateURIPartFinderRegExp() { 802 | if (this._URIPartFinderRegExp_start && this._URIPartFinderRegExp_end) 803 | return; 804 | 805 | const base = this.configs.multibyteEnabled ? 806 | this.findURIPatternMultibytePart : 807 | this.findURIPatternPart ; 808 | this._URIPartFinderRegExp_start = new RegExp(`^(${base})`, 'i'); 809 | this._URIPartFinderRegExp_end = new RegExp(`(${base})$`, 'i'); 810 | }, 811 | 812 | hasLoadableScheme(uri) { 813 | if (!this._schemeRegExp) 814 | this._schemeRegExp = new RegExp(`^(${this.schemes.join('|')}):`, 'i'); 815 | return this._schemeRegExp.test(this.convertFullWidthToHalfWidth(uri)); 816 | }, 817 | _schemeRegExp : null, 818 | 819 | hasScheme(input) { 820 | return this._firstSchemeRegExp.test(input); 821 | }, 822 | removeScheme(input) { 823 | return input.replace(this._firstSchemeRegExp, ''); 824 | }, 825 | get _firstSchemeRegExp() { 826 | if (!this.__firstSchemeRegExp) 827 | this.__firstSchemeRegExp = new RegExp(`^${this.kSchemePatternMultibyte}`, 'i'); 828 | return this.__firstSchemeRegExp; 829 | }, 830 | __firstSchemeRegExp : null, 831 | 832 | fixupURI(uriComponent, aBaseURI) { 833 | const originalURIComponent = uriComponent; 834 | if (this.configs.multibyteEnabled) 835 | uriComponent = this.convertFullWidthToHalfWidth(uriComponent); 836 | 837 | uriComponent = this.sanitizeURIString(uriComponent); 838 | if (!uriComponent) { 839 | log(' => not a URI'); 840 | return null; 841 | } 842 | 843 | uriComponent = this.fixupScheme(uriComponent); 844 | 845 | if (this.configs.relativeEnabled) 846 | uriComponent = this.makeURIComplete(uriComponent, aBaseURI); 847 | 848 | const result = this.hasLoadableScheme(uriComponent) ? uriComponent : null ; 849 | if (result != originalURIComponent) 850 | log(`fixupURI: ${originalURIComponent} => ${result}`); 851 | return result; 852 | }, 853 | 854 | sanitizeURIString(uriComponent) { 855 | const originalURIComponent = uriComponent; 856 | // escape patterns like program codes like JavaScript etc. 857 | if (!this._topLevelDomainsRegExp) { 858 | this._topLevelDomainsRegExp = new RegExp(`^(${this.topLevelDomains.join('|')})$`); 859 | } 860 | if (this.configs.relativeEnabled) { 861 | if ((uriComponent.match(/^([^\/\.]+\.)+([^\/\.]+)$/) && 862 | !RegExp.$2.match(this._topLevelDomainsRegExp)) || 863 | uriComponent.match(/(\(\)|\([^\/]+\)|[;\.,])$/)) 864 | return ''; 865 | } 866 | 867 | uriComponent = this.removeParen(uriComponent); 868 | 869 | while ( 870 | uriComponent.match(/^\((.*)$/) || 871 | uriComponent.match(/^([^\(]*)\)$/) || 872 | uriComponent.match(/^(.*)[\.,]$/) || 873 | uriComponent.match(/^([^\"]*)\"$/) || 874 | uriComponent.match(/^([^\']*)\'$/) || 875 | uriComponent.match(/^(.+)\s*\([^\)]+$/) || 876 | uriComponent.match(/^[^\(]+\)\s*(.+)$/) || 877 | uriComponent.match(/^[^\.\/:]*\((.+)\)[^\.\/]*$/) || 878 | (!this.configs.relativeEnabled && 879 | uriComponent.match(/^[\.\/:](.+)$/)) 880 | ) { 881 | uriComponent = RegExp.$1; 882 | } 883 | 884 | uriComponent = this.removeParen(uriComponent); 885 | 886 | if (this.configs.IDNEnabled || this.configs.i18nPathEnabled) 887 | uriComponent = uriComponent.replace(this.kStringprepReplaceToNothingRegExp, ''); 888 | 889 | if (uriComponent != originalURIComponent) 890 | log(`sanitizeURIString: ${originalURIComponent} => ${uriComponent}`); 891 | return uriComponent; // uriComponent.replace(/^.*\((.+)\).*$/, '$1'); 892 | }, 893 | _topLevelDomainsRegExp : null, 894 | 895 | removeParen(input) { 896 | const originalInput = input; 897 | const doRemoveParen = (aRegExp) => { 898 | const match = input.match(aRegExp); 899 | if (!match) 900 | return false; 901 | input = match[1]; 902 | return true; 903 | }; 904 | while (this._parenPatterns.some(doRemoveParen)) {} 905 | if (input != originalInput) 906 | log(`removeParen: ${originalInput} => ${input}`); 907 | return input; 908 | }, 909 | 910 | fixupScheme(uri) { 911 | const originalURI = uri; 912 | let match = uri.match(this._fixupTargetsRegExp); 913 | if (match) { 914 | const target = match[1]; 915 | let table = this._fixupTable; 916 | for (const pattern of this.niceSplit(this._fixupTargetsPattern, '|')) { 917 | if (new RegExp(pattern).test(target)) 918 | table = table.replace(new RegExp(`\\b${pattern}\\s*=>`), `${target}=>`); 919 | } 920 | match = table.match(new RegExp( 921 | '(?:[,\\| \\n\\r\\t]|^)'+ 922 | target.replace(/([\(\)\+\?\.\{\}])/g, '\\$1') 923 | .replace(/\?/g, '.') 924 | .replace(/\*/g, '.+')+ 925 | '\\s*=>\\s*([^,\\| \\n\\r\\t]+)' 926 | )); 927 | if (match) 928 | uri = uri.replace(target, match[1]); 929 | } 930 | else if (!this._firstSchemeRegExp.test(uri)) { 931 | const scheme = this.configs.schemeFixupDefault; 932 | if (scheme) 933 | uri = `${scheme}://${uri}`; 934 | } 935 | 936 | if (uri != originalURI) 937 | log(`fixupScheme: ${originalURI} => ${uri}`); 938 | return uri; 939 | }, 940 | _fixupTable : '', 941 | _fixupTargets : [], 942 | _fixupTargetsHash : {}, 943 | _fixupTargetsPattern : '', 944 | _fixupTargetsRegExp : '', 945 | 946 | // 相対パスの解決 947 | makeURIComplete(uri, sourceURI) { 948 | if (uri.match(/^(urn|mailto):/i)) 949 | return uri; 950 | 951 | if (uri.match(/^([^\/\.]+\.)+([^\/\.]+)/) && 952 | RegExp.$2.match(new RegExp(`^(${this.topLevelDomains.join('|')})$`))) { 953 | return `${this.configs.schemeFixupDefault}://${uri}`; 954 | } 955 | const base = sourceURI.split('#')[0].split('?')[0].replace(/[^\/]+$/, ''); 956 | return `${base}${uri}`; 957 | }, 958 | 959 | onChangeConfig(key) { 960 | switch (key) { 961 | case 'scheme': 962 | this.scheme = this.configs[key]; 963 | return; 964 | 965 | case 'schemeFixupTable': 966 | this.schemeFixupTable = this.configs[key]; 967 | return; 968 | 969 | case 'IDNScheme': 970 | this.IDNScheme = this.configs[key]; 971 | return; 972 | 973 | case 'relativeEnabled': 974 | case 'IDNEnabled': 975 | case 'i18nPathEnabled': 976 | case 'gTLD': 977 | case 'ccTLD': 978 | case 'IDN_TLD': 979 | case 'extraTLD': 980 | case 'IDNBlacklistChars': 981 | case 'multibyteEnabled': 982 | this.invalidatePatterns(); 983 | return; 984 | 985 | case 'partExceptionWhole': 986 | case 'partExceptionStart': 987 | case 'partExceptionEnd': 988 | this.invalidateExceptionPatterns(); 989 | return; 990 | } 991 | }, 992 | 993 | init: async function(configs) { 994 | log('URIMatcher init with configs ', configs); 995 | if (configs.$loaded) 996 | await configs.$loaded; 997 | 998 | log('URIMatcher init: ready to init'); 999 | this.configs = configs; 1000 | 1001 | this.scheme = configs.scheme; 1002 | this.schemeFixupTable = configs.schemeFixupTable; 1003 | this.IDNScheme = configs.IDNScheme; 1004 | this.allTLD = []; 1005 | configs.$addObserver(this); 1006 | 1007 | this.initialized = (async () => { 1008 | const response = await fetch(browser.extension.getURL('extlib/public_suffix_list.dat')); 1009 | if (!response.ok) 1010 | return false; 1011 | const body = await response.text(); 1012 | this.allTLD = body.replace(/^\/\/.*\n/gm, '').replace(/^\*/gm, '').replace(/\./gm, '\\.').replace(/\n\n+/g, '\n').trim().split('\n'); 1013 | log(this.allTLD.join('\n')); 1014 | return true; 1015 | })(); 1016 | } 1017 | }; 1018 | window.configs && URIMatcher.init(configs); 1019 | --------------------------------------------------------------------------------