├── .cjsescache ├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── demo-large-content.html ├── demo-no-script.html └── demo.html ├── dist └── linkify-plus-plus.user.js ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── rollup.config.mjs └── src ├── extension ├── background.js ├── content.js ├── dialog.js └── options.js ├── lib ├── extension-pref.js ├── main.mjs ├── pref-body.js ├── pref-default.js └── triggers │ ├── cache.mjs │ ├── click.mjs │ ├── hover.mjs │ ├── index.mjs │ ├── load.mjs │ ├── mutation.mjs │ └── util.mjs ├── static ├── _locales │ ├── en │ │ └── messages.json │ └── nl │ │ └── messages.json ├── css │ ├── dialog.css │ └── options.css ├── dialog.html ├── icon-light.svg ├── icon.svg ├── manifest.json └── options.html └── userscript └── index.js /.cjsescache: -------------------------------------------------------------------------------- 1 | [ 2 | "node_modules/event-lite/event-lite.mjs", 3 | "node_modules/webext-pref/lib/promisify.js", 4 | "node_modules/webextension-polyfill/dist/browser-polyfill.js", 5 | "src/lib/extension-pref.js", 6 | "src/lib/pref-body.js", 7 | "src/lib/pref-default.js", 8 | "src/lib/triggers/click.mjs", 9 | "src/lib/triggers/hover.mjs", 10 | "src/lib/triggers/index.mjs", 11 | "src/lib/triggers/load.mjs", 12 | "src/lib/triggers/mutation.mjs" 13 | ] -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 2 3 | indent_style = space 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '18' 13 | - run: npm ci 14 | - run: npm run build 15 | - run: npm test 16 | # - uses: codecov/codecov-action@v3 17 | # with: 18 | # fail_ci_if_error: true 19 | # verbose: true 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | web-ext-artifacts 4 | .eslintcache 5 | dist-extension 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Anthony Lieuallen 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of Anthony Lieuallen nor the names of its contributors may 13 | be used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Linkify Plus Plus 2 | ================= 3 | 4 | [![Build Status](https://travis-ci.com/eight04/linkify-plus-plus.svg?branch=master)](https://travis-ci.com/eight04/linkify-plus-plus) 5 | 6 | A userscript/extension that can linkify almost everything. Based on Linkify Plus. 7 | 8 | See also [linkify-plus-plus-core](https://github.com/eight04/linkify-plus-plus-core). 9 | 10 | Features 11 | -------- 12 | 13 | * Detect text url and convert them into links. 14 | * Support dynamic content. 15 | * Support unicode characters. 16 | * Support custom rules. 17 | * Custom whitelist, blacklist. 18 | * Embed images. 19 | * Multiple methods to trigger the conversion. 20 | 21 | Installation 22 | ------------ 23 | 24 | ### Userscript 25 | 26 | [Install from Greasy Fork](https://greasyfork.org/scripts/4255-linkify-plus-plus). 27 | 28 | ### Firefox extension 29 | 30 | [Install from Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/linkify-plus-plus/). 31 | 32 | ### Chrome extension 33 | 34 | This extension can be installed on Chrome. However, it is not hosted on Chrome Webstore. You have to download the source code and [load the extension as an unpacked extension](https://developer.chrome.com/extensions/getstarted#manifest). 35 | 36 | 1. Go to [release page](https://github.com/eight04/linkify-plus-plus/releases), download and extract the ZIP file. 37 | 2. Navigate to `chrome://extensions/`. 38 | 3. Enable "Developer mode". 39 | 4. Click "LOAD UNPACKED" button and select the folder containing `manifest.json` that is previously extracted. 40 | 41 | Testcase 42 | -------- 43 | 44 | * 45 | * 46 | 47 | Configuration 48 | ------------- 49 | 50 | For the userscript, you can find the configuration from monkey menu. 51 | ![monkey Menu](https://i.imgur.com/Pbdysee.png) 52 | 53 | For the extension, you can open the options page by clicking the browser button. 54 | ![browser button](https://i.imgur.com/bIx3KEM.png) 55 | 56 | Embed images 57 | ------------ 58 | 59 | The script uses the following regular expression to detect images: 60 | 61 | ```js 62 | /^[^?#]+\.(?:jpg|jpeg|png|apng|gif|svg|webp)(?:$|[?#])/i 63 | ``` 64 | 65 | [Source](https://github.com/eight04/linkify-plus-plus-core/blob/5b8da6c2b3fd682585a81028076406ec7592ec37/lib/linkifier.js#L225) 66 | 67 | Custom rules 68 | ------------ 69 | 70 | A list of regex pattern that will be likified, which is aimed to linkify non-http links. For example: 71 | 72 | ``` 73 | magnet:\?xt=\S+ 74 | evernote:///\S+ 75 | ``` 76 | 77 | Build the extension 78 | ------------------- 79 | 80 | Files inside `dist-extension/` and `dist/` are built from the `src/` folder: 81 | 82 | 1. Install Node.js. 83 | 2. Run `npm install` to install dependencies. 84 | 3. Run `npm run build` to build the extension and the userscript. 85 | 86 | Some sites are excluded by default 87 | ---------------------------------- 88 | 89 | You can check the default exclusion list in [package.json](https://github.com/eight04/linkify-plus-plus/blob/master/package.json#L42). The list is also used by the extension. However, since the list is written into the manifest directly, it is not configurable. Unlike the userscript which allows you to modify the list in the script manager. 90 | 91 | File an issue if you think a site should be added/removed from the list. 92 | 93 | Changelog 94 | --------- 95 | 96 | * 12.0.1 (Feb 21, 2025) 97 | 98 | - Fix: remove `strict_min_version` in Firefox extension. 99 | 100 | * 12.0.0 (Feb 21, 2025) 101 | 102 | - Fix: blacklist and whitelist doesn't work with load and mutations triggers. 103 | - Add: onion TLD. 104 | - Change: update Firefox Android version range to 113+. 105 | - Update TLD list. 106 | 107 | * 11.0.0 (Feb 16, 2024) 108 | 109 | - Add: multiple trigger methods in options. 110 | - **Change: now the default trigger is mouse over. switch to page load + new elements for the old behavior.** 111 | 112 | * 10.1.0 (Dec 15, 2021) 113 | 114 | - Fix: allow empty selector in settings. 115 | - Fix: apply the exclude list to the extension. 116 | - Change: exclude mastodon, paypal, term.ptt.cc, 101weiqi.com. 117 | - Translation: add nl. 118 | 119 | * 10.0.0 (Mar 11, 2021) 120 | 121 | - Refactor the entire build process. The userscript no longer `@require` external resource and the extension is smaller. 122 | - Add: Update TLD list. 123 | - Add: mail option. 124 | - Add: embed webp and apng. 125 | - Fix: correctly handle invalid domain labels. 126 | - Fix: match custom rules first. 127 | - Fix: import/export now works in Firefox extension. 128 | 129 | * 9.0.2 (Jun 17, 2019) 130 | 131 | - Fix: custom rules are broken. 132 | - Fix: do not allow users to input broken selector in options. 133 | 134 | * 9.0.1 (Feb 19, 2019) 135 | 136 | - Add: support XHTML pages. 137 | 138 | * 9.0.0 (Aug 27, 2018) 139 | 140 | - **Breaking: replace `GM_config` with `GM_webextPref`. Note that because the configuration system is changed, the script won't be able to read the configuration before version 9.** 141 | - Add: icon. Made by [@FatOrangutan](https://github.com/FatOrangutan). 142 | - Add: webextension build. 143 | - Add: compatible with Greasemonkey 4. Although the script itself can be executed on GM 4, [GM 4 doesn't support monkey menu API](https://github.com/greasemonkey/greasemonkey/issues/2714) so there is no way to open the configuration dialog. 144 | 145 | * Version 8.2.2 (Jul 25, 2018): 146 | - Fix: handle Vue's server side rendering pages. 147 | * Version 8.2.1 (May 23, 2018): 148 | - Fix: LAG. Threads are not correctly marked as started and the processor spawns a bunch of them. 149 | * Version 8.2.0 (May 13, 2018): 150 | - Refactor, use a buffer to queue the elements.. 151 | * Version 8.1.0 (Aug 23, 2017): 152 | - Update linkify-plus-plus-core to 0.3.0. 153 | - Fix: use isContentEditable. 154 | - Add: ability to disable embeding under specified elements. 155 | * Version 8.0.2 (Mar 4, 2017): 156 | - Update linkify-plus-plus-core to 0.2.0. 157 | - Fix blocking bug. 158 | * Version 8.0.1 (Feb 26, 2017): 159 | - Fix global leaking bug in Tampermonkey. 160 | * Version 8.0.0 (Feb 24, 2017): 161 | - Rewritten: the core logic is splitted out as [linkify-plus-plus-core](https://github.com/eight04/linkify-plus-plus-core). 162 | * Version 7.4.4 (Feb 19, 2017): 163 | - Fix: protocol must start with letters. 164 | * Version 7.4.3 (Feb 4, 2017): 165 | - Fix: some js object properties becomes valid TLDs. 166 | - Switch to inline-js. 167 | - Update TLDs. 168 | * Version 7.4.2 (Dec 20, 2016): 169 | - Fix: drop String.includes to support FF38. [something broke](https://greasyfork.org/zh-TW/forum/discussion/13387/x) 170 | * Version 7.4.1 (Dec 7, 2016): 171 | - Fix: empty custom rule bug. 172 | * Version 7.4.0 (Dec 7, 2016): 173 | - Refactor. Add standalone and boundary options. 174 | - Add 4 digit IP option. 175 | - Use TLDs count from domaintools. Remove TLDs whose count <= 2. 176 | * Version 7.3.1 (Nov 8, 2016): 177 | - Update include/exclude rules. 178 | - Update TLD list. 179 | * Version 7.3.0 (Apr 2, 2016): 180 | - Support custom rules. 181 | - Fix leading `_` bug. 182 | * Version 7.2.0 (Feb 15, 2016): 183 | - Don't use mutations if the size is too large. 184 | - Update TLDs. 185 | * Version 7.1.0 (Jan 9, 2016): 186 | - Fix performance issue with mutations. 187 | * Version 7.0.0 (Jan 5, 2016): 188 | - Completely rewrite. The linkification could be stopped during each links. 189 | - Fix performance issue on big text. [Try it yourself](https://rawgit.com/eight04/linkify-plus-plus/master/demo/demo-large-content.html). 190 | - Add `Max execution time` option. 191 | - Replace `ignoreClasses`, `ignoreTags` with CSS selector. 192 | - Limit document.contentType to `text/html` or `text/plain`. 193 | * Version 6.2.1 (Oct 7, 2015): 194 | - Update excluding list, TLDs. 195 | * Version 6.2.0 (Sep 5, 2015): 196 | - Use newest TLDs. 197 | * Version 6.1.0 (Jul 14, 2015): 198 | - Enhance: strip BBCode. 199 | - Enhance: strip trailing question mark. 200 | * Version 6.0.1 (Jul 3, 2015): 201 | - Add alt attribute to image. 202 | * Version 6.0.0 (Jul 3, 2015): 203 | - Enhance: use ES6 generator. 204 | - Fix hanging bug in observer. 205 | - Fix lastIndex issue in createRE. 206 | - Remove config.log. 207 | * Version 5.0.1 (Jun 21, 2015): 208 | - Add compatibility info. 209 | - Fix IN_QUE counting bug. 210 | * Version 5.0.0 (Jun 21, 2015): 211 | - Fix url regex: add valid characters `'[]`. 212 | - Enhance traverser: use TreeWalker to parse DOM tree. 213 | - Enhance white-list: use multiple CSS selectors to specify valid nodes. 214 | - Enhance black-list: be able to remove built-in filter. 215 | - Enhance stripSingleParens: support brackets. 216 | - Add an option to use non-ascii characters in url. 217 | - Remove embedding function. Use [Embed Me!](https://greasyfork.org/zh-TW/scripts/10481-embed-me) instead. 218 | - Update GM_config to 2.0.2. 219 | * Version 4.0.1 (May 8, 2015): 220 | - Fix SVGAnimatedString issue. 221 | * Version 4.0.0 (Apr 27, 2015): 222 | - LPP don't remove `` anymore. Use `Range` to select text. 223 | * Version 3.6.3 (Apr 26, 2015): 224 | - Add `word-wrap: break-word`. 225 | * Version 3.6.2 (Apr 26, 2015): 226 | - Change how tlds work. 227 | * Version 3.6.1 (Apr 23, 2015): 228 | - Use regex to detect angular source. 229 | * Version 3.6.0 (Apr 21, 2015): 230 | - Move embeding function out of LPP core. 231 | * Version 3.5.1 (Apr 17, 2015): 232 | - Use better regex to detect image. 233 | * Version 3.5.0 (Apr 16, 2015): 234 | - Use a different GM_config library. 235 | * Version 3.4.2 (Apr 16, 2015): 236 | - Add spreadsheetinfo to ignore list. 237 | * Version 3.4.1 (Apr 16, 2015): 238 | - Fix className issue. Move CSS rule to style.css. 239 | * Version 3.4.0 (Apr 13, 2015): 240 | - New feature: Embed youtube video. 241 | * Version 3.3.0 (Apr 1, 2015): 242 | - New feature: Open link in new tab. 243 | * Version 3.2.6 (Apr 1, 2015): 244 | - Tampermonkey dosn't support magic TLD. 245 | * Version 3.2.5 (Feb 20, 2015): 246 | - Make path match "," character. 247 | * Version 3.2.4 (Jan 30, 2015): 248 | - Make user part of url match "-+" characters. 249 | * Version 3.2.3 (Jan 21, 2015): 250 | - Fix parentNode == null bug in validRoot(). 251 | * Version 3.2.2 (Jan 9, 2015): 252 | - Fix root node validation bug. 253 | * Version 3.2.1 (Jan 9, 2015): 254 | - Fix class matching bug. 255 | * Version 3.2.0 (Dec 20, 2014): 256 | - Add `config.generateLog` option to config. 257 | * Version 3.1.1 (Dec 5, 2014): 258 | - Add `https?://www.google.tld/webhp*` to excluding list. 259 | * Version 3.1.0 (Nov 15, 2014): 260 | - Remove removeWBR(). Now the script will check `wbr` when traversing DOM. 261 | * Version 3.0.6 (Nov 15, 2014): 262 | - Fixed potential bug that root could be invalid after removing ``. 263 | * Version 3.0.5 (Nov 14, 2014): 264 | - Fixed tag name excluding bug. 265 | * Version 3.0.4 (Nov 14, 2014): 266 | - Add style. 267 | * Version 3.0.3 (Nov 14, 2014): 268 | - Fixed comment element bug. 269 | * Version 3.0.2 (Nov 14, 2014): 270 | - Fixed traverse bug. 271 | * Version 3.0.1 (Nov 14, 2014): 272 | - Cleanup console.log. 273 | * Version 3.0.0 (Nov 14, 2014): 274 | - Breaking change. Removed linkifyContainer and use new DOM traversal method. 275 | * Version 2.4.3 (Nov 7, 2014): 276 | - Add custom class name black-list. 277 | * Version 2.4.2 (Nov 7, 2014): 278 | - Add .bdsug to ignore list. 279 | * Version 2.4.1 (Nov 7, 2014): 280 | - Ignore if @contenteditable attribute is true. 281 | * Version 2.4.0 (Nov 7, 2014): 282 | - Add class whitelist. Set your cutom whitelist in config dialog. 283 | * Version 2.3.25 (Nov 7, 2014): 284 | - Fix wbr removing bug. 285 | * Version 2.3.24 (Nov 5, 2014): 286 | - Fix parenthesis match point. 287 | * Version 2.3.23 (Nov 5, 2014): 288 | - Add parenthesis support. 289 | * Version 2.3.22 (Oct 20, 2014): 290 | - Add ".brush:" to ignore list. 291 | * Version 2.3.21 (Sep 30, 2014): 292 | - Fix: max node limits... 293 | * Version 2.3.20 (Sep 29, 2014): 294 | - Enhance: Increase performance. 295 | * Version 2.3.19 (Sep 29, 2014): 296 | - Fix: Reduce node limits. 297 | * Version 2.3.18 (Sep 18, 2014): 298 | - Fix: Add h* tags into ignore list. 299 | * Version 2.3.17 (Sep 16, 2014): 300 | - Enhance: Delay the script execution if there are too many nodes. 301 | * Version 2.3.16 (Sep 15, 2014): 302 | - Enhance: Use better config style. 303 | * Version 2.3.15 (Sep 15, 2014): 304 | - Fix: Grant bug (again) 305 | * Version 2.3.14 (Sep 15, 2014): 306 | - Add: GM_config. Deside whether to display image. 307 | * Version 2.3.13 (Sep 15, 2014): 308 | - Fix: Remove before linkify. 309 | * Version 2.3.12 (Sep 9, 2014): 310 | - Fix: Angular conflict. Check "{{ }}" pairs. 311 | * Version 2.3.11 (Sep 7, 2014): 312 | - Enhance: add isIP function. 313 | * Version 2.3.10 (Sep 7, 2014): 314 | - Enhance: Use better ip detection 315 | * Version 2.3.9 (Sep 7, 2014): 316 | - Fix: Add domain check for ip numbers. 317 | - Fix: Add domain check ".." invalid. 318 | * Version 2.3.8 (Sep 6, 2014): 319 | - Fix: Push to event queue to avoid Angular conflict. It should work on most of pages. 320 | * Version 2.3.7 (Sep 6, 2014): 321 | - Fix: Match port and hyphen in domain. 322 | * Version 2.3.6 (Sep 4, 2014): 323 | - Fix: Angular conflict. 324 | * Version 2.3.5 (Sep 3, 2014): 325 | - Remove image when loading failed. 326 | - Remove debug code. 327 | * Version 2.3.4 (Sep 3, 2014): 328 | - Since FF 32 have some problem dealing with unicode, use a new path RE. 329 | * Version 2.3.3 (Sep 2, 2014): 330 | - Add svg and some new tags to ignore list. 331 | * Version 2.3.2 (Sep 2, 2014): 332 | - Add ttp:// -> http:// alia. 333 | - Use TLD list! 334 | * Version 2.3.1 (Sep 1, 2014): 335 | - Move class tester into xpath. 336 | * Version 2.3 (Sep 1, 2014): 337 | - Match to a pretty large set. Check readme for detail. 338 | * Version 2.2.2 (Aug 26, 2014): 339 | - Add .code to ignore list. 340 | * Version 2.2.1 (Aug 17, 2014): 341 | - Ignore .highlight container. 342 | * Version 2.2 (Aug 15, 2014): 343 | - Add images support. 344 | - Use Observer instead of DOMNodeInserted. 345 | * Version 2.1.4 (Aug 12, 2012): 346 | - Bug fix for when (only some) nodes have been removed from the document. 347 | * Version 2.1.3 (Oct 24, 2011): 348 | - More excludes. 349 | * Version 2.1.2: 350 | - Some bug fixes. 351 | * Version 2.1.1: 352 | - Ignore certain "highlighter" script containers. 353 | * Version 2.1: 354 | - Rewrite the regular expression to be more readable. 355 | - Fix trailing "." characters. 356 | * Version 2.0.3: 357 | - Fix infinite recursion on X(HT)ML pages. 358 | * Version 2.0.2: 359 | - Limit @include, for greater site/plugin compatibility. 360 | * Version 2.0.1: 361 | - Fix aberrant 'mailto:' where it does not belong. 362 | * Version 2.0: 363 | - Apply incrementally, so the browser does not hang on large pages. 364 | - Continually apply to new content added to the page (i.e. AJAX). 365 | * Version 1.1.4: 366 | - Basic "don't screw up xml pretty printing" exception case 367 | * Version 1.1.3: 368 | - Include "+" in the username of email addresses. 369 | * Version 1.1.2: 370 | - Include "." in the username of email addresses. 371 | * Version 1.1: 372 | - Fixed a big that caused the first link in a piece of text to be skipped (i.e. not linkified). 373 | -------------------------------------------------------------------------------- /demo/demo-no-script.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Linkify Plus Plus 7 | 30 | 31 | 32 | 46 | 47 | 48 |
49 |

Linkify Plus Plus Test Page

50 | 51 |
52 |

Random test 1

53 | 54 | Original from http://piro.sakura.ne.jp/xul/textlink/index.html.en#testcases 55 | 56 |

57 | Mozilla(http://www.mozilla.org/) started as a project of next generation www-browser of Netscape(http://www.netscape.com/). See http://www.mozilla.org/src-faq.html#1. Mozilla were planned to be released as "Netscape Communicator 5.0", but the new layout engine "NGLayout" ttp://www.mozilla.org/newlayout/gecko.html prevented it. Netscape 6 ttp://ftp.netscape.com/pub/netscape6/ was released after 2 years from the deciding.And now, the Firefox(h**p://www.mozilla.com/firefox/) web browser is released from the Mozilla Corporation(h++p://www.mozilla.com/). 58 |

59 |

60 | Now, TextLink can parse splitted text nodes as joined texts. Mozilla(http://www.mozilla.org/) started as a project of next generation www-browser of Netscape(http://www.netscape.com/). See http://www.mozilla.org/src-faq.html#1. Mozilla were planned to be released as "Netscape Communicator 5.0", but the new layout engine "NGLayout" ttp://www.mozilla.org/newlayout/gecko.html prevented it. Netscape 6 ttp://ftp.netscape.com/pub/netscape6/ was released after 2 years from the deciding. 61 |

62 |

63 | There are relative links. Firefox www.mozilla.org/products/firefox/ is a new type www-browser and it very extendable. You can find many extensions from update.mozilla.org. I also release some extensions. TBE ./tabextensions/index.html.en and PopupALT _popupalt.html.en are parts of them. (Text Link ignores relative pathes which includes only a filename like "_popupalt.html.en", because strings with the pattern usually indicates only filenames, not relative pathes.) See the parent page ../xul/xul.html to find other extensions in this website. 64 |

65 |

66 | There are links including multi-byte characters. The URI of this website is "http://piro.sakura.ne.jp/", but you can use the secondary URI, "ttp://www98.sakura.ne.jp/ ̄piro/" too. 67 |

68 | Also I don't want these: http://ptt.cc/1234WITHUNICODE, http://example.org/, http://example.org/,http://example.org, 127.0.0.1, http://127.0.0.1, telnet://ptt.cc, AFTERUNICODEhttp://example.org, I'm so happy www, www.sohappywwwhaha, www.... 69 |

70 | 71 |

Random test 2

72 | 73 | Original from http://yellow5.us/firefox/testcases.txt 74 | 75 |

76 | ABOUT 77 | about:blank 78 | about:config 79 | 80 | MAILTO 81 | mailto:example@example.com 82 | 83 | HTTP 84 | http://www.example.com 85 | http://www.example.com. 86 | http://www.example.com/test/ 87 | http://www.example.com/test/. 88 | http://www.example.com/test/index.html 89 | http://www.example.com/test/index.html. 90 | http://user@www.example.com 91 | http://user@www.example.com. 92 | http://user@www.example.com/test/ 93 | http://user@www.example.com/test/. 94 | http://user@www.example.com/test/index.html 95 | http://user@www.example.com/test/index.html. 96 | http://user:password@www.example.com 97 | http://user:password@www.example.com. 98 | http://user:password@www.example.com/test/ 99 | http://user:password@www.example.com/test/. 100 | http://user:password@www.example.com/test/index.html 101 | http://user:password@www.example.com/test/index.html. 102 | http://192.168.0.1 103 | http://192.168.0.1. 104 | http://192.168.0.1/test/ 105 | http://192.168.0.1/test/. 106 | http://192.168.0.1/test/index.html 107 | http://192.168.0.1/test/index.html. 108 | http://user@192.168.0.1 109 | http://user@192.168.0.1. 110 | http://user@192.168.0.1/test/ 111 | http://user@192.168.0.1/test/. 112 | http://user@192.168.0.1/test/index.html 113 | http://user@192.168.0.1/test/index.html. 114 | http://user:password@192.168.0.1 115 | http://user:password@192.168.0.1. 116 | http://user:password@192.168.0.1/test/ 117 | http://user:password@192.168.0.1/test/. 118 | http://user:password@192.168.0.1/test/index.html 119 | http://user:password@192.168.0.1/test/index.html. 120 | 121 | HTTPS 122 | https://www.example.com 123 | https://www.example.com. 124 | https://www.example.com/test/ 125 | https://www.example.com/test/. 126 | https://www.example.com/test/index.html 127 | https://www.example.com/test/index.html. 128 | https://user@www.example.com 129 | https://user@www.example.com. 130 | https://user@www.example.com/test/ 131 | https://user@www.example.com/test/. 132 | https://user@www.example.com/test/index.html 133 | https://user@www.example.com/test/index.html. 134 | https://user:password@www.example.com 135 | https://user:password@www.example.com. 136 | https://user:password@www.example.com/test/ 137 | https://user:password@www.example.com/test/. 138 | https://user:password@www.example.com/test/index.html 139 | https://user:password@www.example.com/test/index.html. 140 | https://192.168.0.1 141 | https://192.168.0.1. 142 | https://192.168.0.1/test/ 143 | https://192.168.0.1/test/. 144 | https://192.168.0.1/test/index.html 145 | https://192.168.0.1/test/index.html. 146 | https://user@192.168.0.1 147 | https://user@192.168.0.1. 148 | https://user@192.168.0.1/test/ 149 | https://user@192.168.0.1/test/. 150 | https://user@192.168.0.1/test/index.html 151 | https://user@192.168.0.1/test/index.html. 152 | https://user:password@192.168.0.1 153 | https://user:password@192.168.0.1. 154 | https://user:password@192.168.0.1/test/ 155 | https://user:password@192.168.0.1/test/. 156 | https://user:password@192.168.0.1/test/index.html 157 | https://user:password@192.168.0.1/test/index.html. 158 | 159 | FTP 160 | ftp://www.example.com 161 | ftp://www.example.com. 162 | ftp://www.example.com/test/ 163 | ftp://www.example.com/test/. 164 | ftp://www.example.com/test/index.html 165 | ftp://www.example.com/test/index.html. 166 | ftp://user@www.example.com 167 | ftp://user@www.example.com. 168 | ftp://user@www.example.com/test/ 169 | ftp://user@www.example.com/test/. 170 | ftp://user@www.example.com/test/index.html 171 | ftp://user@www.example.com/test/index.html. 172 | ftp://user:password@www.example.com 173 | ftp://user:password@www.example.com. 174 | ftp://user:password@www.example.com/test/ 175 | ftp://user:password@www.example.com/test/. 176 | ftp://user:password@www.example.com/test/index.html 177 | ftp://user:password@www.example.com/test/index.html. 178 | ftp://192.168.0.1 179 | ftp://192.168.0.1. 180 | ftp://192.168.0.1/test/ 181 | ftp://192.168.0.1/test/. 182 | ftp://192.168.0.1/test/index.html 183 | ftp://192.168.0.1/test/index.html. 184 | ftp://user@192.168.0.1 185 | ftp://user@192.168.0.1. 186 | ftp://user@192.168.0.1/test/ 187 | ftp://user@192.168.0.1/test/. 188 | ftp://user@192.168.0.1/test/index.html 189 | ftp://user@192.168.0.1/test/index.html. 190 | ftp://user:password@192.168.0.1 191 | ftp://user:password@192.168.0.1. 192 | ftp://user:password@192.168.0.1/test/ 193 | ftp://user:password@192.168.0.1/test/. 194 | ftp://user:password@192.168.0.1/test/index.html 195 | ftp://user:password@192.168.0.1/test/index.html. 196 | 197 | NNTP 198 | nntp://www.example.com 199 | nntp://www.example.com. 200 | nntp://www.example.com/test/ 201 | nntp://www.example.com/test/. 202 | nntp://www.example.com/test/index.html 203 | nntp://www.example.com/test/index.html. 204 | nntp://user@www.example.com 205 | nntp://user@www.example.com. 206 | nntp://user@www.example.com/test/ 207 | nntp://user@www.example.com/test/. 208 | nntp://user@www.example.com/test/index.html 209 | nntp://user@www.example.com/test/index.html. 210 | nntp://user:password@www.example.com 211 | nntp://user:password@www.example.com. 212 | nntp://user:password@www.example.com/test/ 213 | nntp://user:password@www.example.com/test/. 214 | nntp://user:password@www.example.com/test/index.html 215 | nntp://user:password@www.example.com/test/index.html. 216 | nntp://192.168.0.1 217 | nntp://192.168.0.1. 218 | nntp://192.168.0.1/test/ 219 | nntp://192.168.0.1/test/. 220 | nntp://192.168.0.1/test/index.html 221 | nntp://192.168.0.1/test/index.html. 222 | nntp://user@192.168.0.1 223 | nntp://user@192.168.0.1. 224 | nntp://user@192.168.0.1/test/ 225 | nntp://user@192.168.0.1/test/. 226 | nntp://user@192.168.0.1/test/index.html 227 | nntp://user@192.168.0.1/test/index.html. 228 | nntp://user:password@192.168.0.1 229 | nntp://user:password@192.168.0.1. 230 | nntp://user:password@192.168.0.1/test/ 231 | nntp://user:password@192.168.0.1/test/. 232 | nntp://user:password@192.168.0.1/test/index.html 233 | nntp://user:password@192.168.0.1/test/index.html. 234 | 235 | NEWS 236 | news://www.example.com 237 | news://www.example.com. 238 | news://www.example.com/test/ 239 | news://www.example.com/test/. 240 | news://www.example.com/test/index.html 241 | news://www.example.com/test/index.html. 242 | news://user@www.example.com 243 | news://user@www.example.com. 244 | news://user@www.example.com/test/ 245 | news://user@www.example.com/test/. 246 | news://user@www.example.com/test/index.html 247 | news://user@www.example.com/test/index.html. 248 | news://user:password@www.example.com 249 | news://user:password@www.example.com. 250 | news://user:password@www.example.com/test/ 251 | news://user:password@www.example.com/test/. 252 | news://user:password@www.example.com/test/index.html 253 | news://user:password@www.example.com/test/index.html. 254 | news://192.168.0.1 255 | news://192.168.0.1. 256 | news://192.168.0.1/test/ 257 | news://192.168.0.1/test/. 258 | news://192.168.0.1/test/index.html 259 | news://192.168.0.1/test/index.html. 260 | news://user@192.168.0.1 261 | news://user@192.168.0.1. 262 | news://user@192.168.0.1/test/ 263 | news://user@192.168.0.1/test/. 264 | news://user@192.168.0.1/test/index.html 265 | news://user@192.168.0.1/test/index.html. 266 | news://user:password@192.168.0.1 267 | news://user:password@192.168.0.1. 268 | news://user:password@192.168.0.1/test/ 269 | news://user:password@192.168.0.1/test/. 270 | news://user:password@192.168.0.1/test/index.html 271 | news://user:password@192.168.0.1/test/index.html. 272 | 273 | TELNET 274 | telnet://www.example.com 275 | telnet://www.example.com. 276 | telnet://www.example.com/test/ 277 | telnet://www.example.com/test/. 278 | telnet://www.example.com/test/index.html 279 | telnet://www.example.com/test/index.html. 280 | telnet://user@www.example.com 281 | telnet://user@www.example.com. 282 | telnet://user@www.example.com/test/ 283 | telnet://user@www.example.com/test/. 284 | telnet://user@www.example.com/test/index.html 285 | telnet://user@www.example.com/test/index.html. 286 | telnet://user:password@www.example.com 287 | telnet://user:password@www.example.com. 288 | telnet://user:password@www.example.com/test/ 289 | telnet://user:password@www.example.com/test/. 290 | telnet://user:password@www.example.com/test/index.html 291 | telnet://user:password@www.example.com/test/index.html. 292 | telnet://192.168.0.1 293 | telnet://192.168.0.1. 294 | telnet://192.168.0.1/test/ 295 | telnet://192.168.0.1/test/. 296 | telnet://192.168.0.1/test/index.html 297 | telnet://192.168.0.1/test/index.html. 298 | telnet://user@192.168.0.1 299 | telnet://user@192.168.0.1. 300 | telnet://user@192.168.0.1/test/ 301 | telnet://user@192.168.0.1/test/. 302 | telnet://user@192.168.0.1/test/index.html 303 | telnet://user@192.168.0.1/test/index.html. 304 | telnet://user:password@192.168.0.1 305 | telnet://user:password@192.168.0.1. 306 | telnet://user:password@192.168.0.1/test/ 307 | telnet://user:password@192.168.0.1/test/. 308 | telnet://user:password@192.168.0.1/test/index.html 309 | telnet://user:password@192.168.0.1/test/index.html. 310 | 311 | IRC 312 | irc://www.example.com 313 | irc://www.example.com. 314 | irc://www.example.com/test/ 315 | irc://www.example.com/test/. 316 | irc://www.example.com/test/index.html 317 | irc://www.example.com/test/index.html. 318 | irc://user@www.example.com 319 | irc://user@www.example.com. 320 | irc://user@www.example.com/test/ 321 | irc://user@www.example.com/test/. 322 | irc://user@www.example.com/test/index.html 323 | irc://user@www.example.com/test/index.html. 324 | irc://user:password@www.example.com 325 | irc://user:password@www.example.com. 326 | irc://user:password@www.example.com/test/ 327 | irc://user:password@www.example.com/test/. 328 | irc://user:password@www.example.com/test/index.html 329 | irc://user:password@www.example.com/test/index.html. 330 | irc://192.168.0.1 331 | irc://192.168.0.1. 332 | irc://192.168.0.1/test/ 333 | irc://192.168.0.1/test/. 334 | irc://192.168.0.1/test/index.html 335 | irc://192.168.0.1/test/index.html. 336 | irc://user@192.168.0.1 337 | irc://user@192.168.0.1. 338 | irc://user@192.168.0.1/test/ 339 | irc://user@192.168.0.1/test/. 340 | irc://user@192.168.0.1/test/index.html 341 | irc://user@192.168.0.1/test/index.html. 342 | irc://user:password@192.168.0.1 343 | irc://user:password@192.168.0.1. 344 | irc://user:password@192.168.0.1/test/ 345 | irc://user:password@192.168.0.1/test/. 346 | irc://user:password@192.168.0.1/test/index.html 347 | irc://user:password@192.168.0.1/test/index.html. 348 | 349 | CUSTOM 350 | hxxp://www.example.com 351 | hxxp://www.example.com. 352 | hxxp://www.example.com/test/ 353 | hxxp://www.example.com/test/. 354 | hxxp://www.example.com/test/index.html 355 | hxxp://www.example.com/test/index.html. 356 | hxxp://user@www.example.com 357 | hxxp://user@www.example.com. 358 | hxxp://user@www.example.com/test/ 359 | hxxp://user@www.example.com/test/. 360 | hxxp://user@www.example.com/test/index.html 361 | hxxp://user@www.example.com/test/index.html. 362 | hxxp://user:password@www.example.com 363 | hxxp://user:password@www.example.com. 364 | hxxp://user:password@www.example.com/test/ 365 | hxxp://user:password@www.example.com/test/. 366 | hxxp://user:password@www.example.com/test/index.html 367 | hxxp://user:password@www.example.com/test/index.html. 368 | hxxp://192.168.0.1 369 | hxxp://192.168.0.1. 370 | hxxp://192.168.0.1/test/ 371 | hxxp://192.168.0.1/test/. 372 | hxxp://192.168.0.1/test/index.html 373 | hxxp://192.168.0.1/test/index.html. 374 | hxxp://user@192.168.0.1 375 | hxxp://user@192.168.0.1. 376 | hxxp://user@192.168.0.1/test/ 377 | hxxp://user@192.168.0.1/test/. 378 | hxxp://user@192.168.0.1/test/index.html 379 | hxxp://user@192.168.0.1/test/index.html. 380 | hxxp://user:password@192.168.0.1 381 | hxxp://user:password@192.168.0.1. 382 | hxxp://user:password@192.168.0.1/test/ 383 | hxxp://user:password@192.168.0.1/test/. 384 | hxxp://user:password@192.168.0.1/test/index.html 385 | hxxp://user:password@192.168.0.1/test/index.html. 386 | 387 | CUSTOM 388 | h**p://www.example.com 389 | h**p://www.example.com. 390 | h**p://www.example.com/test/ 391 | h**p://www.example.com/test/. 392 | h**p://www.example.com/test/index.html 393 | h**p://www.example.com/test/index.html. 394 | h**p://user@www.example.com 395 | h**p://user@www.example.com. 396 | h**p://user@www.example.com/test/ 397 | h**p://user@www.example.com/test/. 398 | h**p://user@www.example.com/test/index.html 399 | h**p://user@www.example.com/test/index.html. 400 | h**p://user:password@www.example.com 401 | h**p://user:password@www.example.com. 402 | h**p://user:password@www.example.com/test/ 403 | h**p://user:password@www.example.com/test/. 404 | h**p://user:password@www.example.com/test/index.html 405 | h**p://user:password@www.example.com/test/index.html. 406 | h**p://192.168.0.1 407 | h**p://192.168.0.1. 408 | h**p://192.168.0.1/test/ 409 | h**p://192.168.0.1/test/. 410 | h**p://192.168.0.1/test/index.html 411 | h**p://192.168.0.1/test/index.html. 412 | h**p://user@192.168.0.1 413 | h**p://user@192.168.0.1. 414 | h**p://user@192.168.0.1/test/ 415 | h**p://user@192.168.0.1/test/. 416 | h**p://user@192.168.0.1/test/index.html 417 | h**p://user@192.168.0.1/test/index.html. 418 | h**p://user:password@192.168.0.1 419 | h**p://user:password@192.168.0.1. 420 | h**p://user:password@192.168.0.1/test/ 421 | h**p://user:password@192.168.0.1/test/. 422 | h**p://user:password@192.168.0.1/test/index.html 423 | h**p://user:password@192.168.0.1/test/index.html. 424 | 425 | WWW (no protocol) 426 | www.example.com 427 | www.example.com. 428 | www.example.com/test/ 429 | www.example.com/test/. 430 | www.example.com/test/index.html 431 | www.example.com/test/index.html. 432 | user@www.example.com (ambiguous, but recognized subdomain. not an e-mail address) 433 | user@www.example.com. (ambiguous, but recognized subdomain. not an e-mail address) 434 | user@www.example.com/test/ 435 | user@www.example.com/test/. 436 | user@www.example.com/test/index.html 437 | user@www.example.com/test/index.html. 438 | user:password@www.example.com 439 | user:password@www.example.com. 440 | user:password@www.example.com/test/ 441 | user:password@www.example.com/test/. 442 | user:password@www.example.com/test/index.html 443 | user:password@www.example.com/test/index.html. 444 | 445 | FTP (no protocol) 446 | ftp.example.com 447 | ftp.example.com. 448 | ftp.example.com/test/ 449 | ftp.example.com/test/. 450 | ftp.example.com/test/index.html 451 | ftp.example.com/test/index.html. 452 | user@ftp.example.com (ambiguous, but recognized subdomain. not an e-mail address) 453 | user@ftp.example.com. (ambiguous, but recognized subdomain. not an e-mail address) 454 | user@ftp.example.com/test/ 455 | user@ftp.example.com/test/. 456 | user@ftp.example.com/test/index.html 457 | user@ftp.example.com/test/index.html. 458 | user:password@ftp.example.com 459 | user:password@ftp.example.com. 460 | user:password@ftp.example.com/test/ 461 | user:password@ftp.example.com/test/. 462 | user:password@ftp.example.com/test/index.html 463 | user:password@ftp.example.com/test/index.html. 464 | 465 | IRC (no protocol) 466 | irc.example.com 467 | irc.example.com. 468 | irc.example.com/test/ 469 | irc.example.com/test/. 470 | irc.example.com/test/index.html 471 | irc.example.com/test/index.html. 472 | user@irc.example.com (ambiguous, but recognized subdomain. not an e-mail address) 473 | user@irc.example.com. (ambiguous, but recognized subdomain. not an e-mail address) 474 | user@irc.example.com/test/ 475 | user@irc.example.com/test/. 476 | user@irc.example.com/test/index.html 477 | user@irc.example.com/test/index.html. 478 | user:password@irc.example.com 479 | user:password@irc.example.com. 480 | user:password@irc.example.com/test/ 481 | user:password@irc.example.com/test/. 482 | user:password@irc.example.com/test/index.html 483 | user:password@irc.example.com/test/index.html. 484 | #test-name@irc.example.com 485 | #test-name@irc.example.com. 486 | irc.example.com#test-name 487 | irc.example.com#test-name. 488 | 489 | IP (no protocol) 490 | 192.168.0.1 (not linkified; pattern too common) 491 | 192.168.0.1. (not linkified; pattern too common) 492 | 192.168.0.1/test/ 493 | 192.168.0.1/test/. 494 | 192.168.0.1/test/index.html 495 | 192.168.0.1/test/index.html. 496 | user@192.168.0.1 (ambiguous; should be recognized as e-mail) 497 | user@192.168.0.1. (ambiguous; should be recognized as e-mail) 498 | user@192.168.0.1/test/ 499 | user@192.168.0.1/test/. 500 | user@192.168.0.1/test/index.html 501 | user@192.168.0.1/test/index.html. 502 | user:password@192.168.0.1 503 | user:password@192.168.0.1. 504 | user:password@192.168.0.1/test/ 505 | user:password@192.168.0.1/test/. 506 | user:password@192.168.0.1/test/index.html 507 | user:password@192.168.0.1/test/index.html. 508 | 509 | OTHER (no protocol) 510 | subdomain.example.com (not linkified; pattern too common) 511 | subdomain.example.com. (not linkified; pattern too common) 512 | subdomain.example.com/test/ 513 | subdomain.example.com/test/. 514 | subdomain.example.com/test/index.html 515 | subdomain.example.com/test/index.html. 516 | user@subdomain.example.com (ambiguous; should be recognized as e-mail) 517 | user@subdomain.example.com. (ambiguous; should be recognized as e-mail) 518 | user@subdomain.example.com/test/ 519 | user@subdomain.example.com/test/. 520 | user@subdomain.example.com/test/index.html 521 | user@subdomain.example.com/test/index.html. 522 | user:password@subdomain.example.com 523 | user:password@subdomain.example.com. 524 | user:password@subdomain.example.com/test/ 525 | user:password@subdomain.example.com/test/. 526 | user:password@subdomain.example.com/test/index.html 527 | user:password@subdomain.example.com/test/index.html. 528 | 529 | EMAIL 530 | test@example.com 531 | test@example.com. 532 | test.test@test.example.com 533 | test.test@test.example.com. 534 | test@192.168.0.1 535 | test@192.168.0.1. 536 | test.test@192.168.0.1 537 | test.test@192.168.0.1. 538 | 539 | IMAGE 540 | http://www.example.com/image.jpg 541 | http://www.example.com/image.jpeg 542 | http://www.example.com/image.png 543 | http://www.example.com/image.gif 544 | http://www.example.com/image.bmp 545 | http://www.example.com/image.jpg.test (not an image) 546 | http://www.example.com/image.jpeg.test (not an image) 547 | http://www.example.com/image.png.test (not an image) 548 | http://www.example.com/image.gif.test (not an image) 549 | http://www.example.com/image.bmp.test (not an image) 550 | http://www.example.com/image.jpg?test 551 | http://www.example.com/image.jpeg?test 552 | http://www.example.com/image.png?test 553 | http://www.example.com/image.gif?test 554 | http://www.example.com/image.bmp?test 555 | http://www.example.com/image.test?jpg (not an image) 556 | http://www.example.com/image.test?jpeg (not an image) 557 | http://www.example.com/image.test?png (not an image) 558 | http://www.example.com/image.test?gif (not an image) 559 | http://www.example.com/image.test?bmp (not an image) 560 | http://www.example.com/image.jpg#test 561 | http://www.example.com/image.jpeg#test 562 | http://www.example.com/image.png#test 563 | http://www.example.com/image.gif#test 564 | http://www.example.com/image.bmp#test 565 | http://www.example.com/image.test#jpg (not an image) 566 | http://www.example.com/image.test#jpeg (not an image) 567 | http://www.example.com/image.test#png (not an image) 568 | http://www.example.com/image.test#gif (not an image) 569 | http://www.example.com/image.test#bmp (not an image) 570 | https://greasyfork.org/assets/blacklogo96-0596aff6108f83c3073764496d7768ec.png 571 | http://i.imgur.com/25zhGbg.jpg 572 | https://f061172b00c7bca1e36fdd56f00f238cf2545831.googledrive.com/host/0B_P4A1paVEPbb1UxSUdua3Fwc1k/Vorago_chathead.png 573 | https://secure.runescape.com/m=weblogin/logout.ws?.png 574 | 575 | Dots 576 | http://www.example.com 577 | http://www.example.com. 578 | http://www.example.com.. 579 | http://www.example.com... 580 | http://www.example.com/ 581 | http://www.example.com/. 582 | http://www.example.com/.. 583 | http://www.example.com/... 584 | http://www.example.com/ 585 | http://www.example.com/. 586 | http://www.example.com/./. 587 | http://www.example.com/../. 588 |

589 | 590 |

Random test 3

591 | 592 | Original from http://markdown-it.github.io/linkify-it/, licensed under MIT 593 | 594 |

595 | % 596 | % Regular links 597 | % 598 | My http://example.com site 599 | My http://example.com/ site 600 | http://example.com/foo_bar/ 601 | http://user:pass@example.com:8080 602 | http://user@example.com 603 | http://user@example.com:8080 604 | http://user:pass@example.com 605 | [https](https://www.ibm.com)[mailto](mailto:someone@ibm.com) % should not catch as auth (before @ in big link) 606 | http://example.com:8080 607 | http://example.com/?foo=bar 608 | http://example.com?foo=bar 609 | http://example.com/#foo=bar 610 | http://example.com#foo=bar 611 | http://a.in 612 | HTTP://GOOGLE.COM 613 | http://example.invalid % don't restrict root domain when schema exists 614 | http://inrgess2 % Allow local domains to end with digit 615 | http://999 % ..and start with digit, and have digits only 616 | http://host-name % local domain with dash 617 | >>example.com % markdown blockquote 618 | >>http://example.com % markdown blockquote 619 | http://lyricstranslate.com/en/someone-you-നിന്നെ-പോലൊരാള്‍.html % With control character 620 | 621 | % 622 | % localhost (only with protocol allowed) 623 | % 624 | //localhost 625 | //test.123 626 | http://localhost:8000? 627 | 628 | % 629 | % Other protocols 630 | % 631 | My ssl https://example.com site 632 | My ftp://example.com site 633 | 634 | % 635 | % Neutral proto 636 | % 637 | My ssl //example.com site 638 | 639 | % 640 | % IPs 641 | % 642 | 4.4.4.4 643 | 192.168.1.1/abc 644 | 645 | % 646 | % Fuzzy 647 | % 648 | test.example@http://vk.com 649 | text:http://example.com/ 650 | google.com 651 | google.com: // no port 652 | s.l.o.w.io 653 | a-b.com 654 | GOOGLE.COM. 655 | google.xxx // known tld 656 | 657 | % 658 | % Correct termination for . , ! ? [] {} () "" '' 659 | % 660 | (Scoped http://example.com/foo_bar) 661 | http://example.com/foo_bar_(wiki) 662 | http://foo.com/blah_blah_[other] 663 | http://foo.com/blah_blah_{I'm_king} 664 | http://foo.com/blah_blah_I'm_king 665 | http://www.kmart.com/bestway-10'-x-30inch-steel-pro-frame-pool/p-004W007538417001P 666 | http://foo.com/blah_blah_"doublequoted" 667 | http://foo.com/blah_blah_'singlequoted' 668 | (Scoped like http://example.com/foo_bar) 669 | [Scoped like http://example.com/foo_bar] 670 | {Scoped like http://example.com/foo_bar} 671 | "Quoted like http://example.com/foo_bar" 672 | 'Quoted like http://example.com/foo_bar' 673 | [example.com/foo_bar.jpg)] 674 | http://example.com/foo_bar.jpg. 675 | http://example.com/foo_bar/. 676 | http://example.com/foo_bar, 677 | https://github.com/markdown-it/linkify-it/compare/360b13a733f521a8d4903d3a5e1e46c357e9d3ce...f580766349525150a80a32987bb47c2d592efc33 678 | http://example.com/foo_bar... 679 | http://172.26.142.48/viewerjs/#../0529/slides.pdf 680 | http://example.com/foo_bar.. 681 | http://example.com/foo_bar?p=10. 682 | https://www.google.ru/maps/@59.9393895,30.3165389,15z?hl=ru 683 | https://www.google.com/maps/place/New+York,+NY,+USA/@40.702271,-73.9968471,11z/data=!4m2!3m1!1s0x89c24fa5d33f083b:0xc80b8f06e177fe62?hl=en 684 | https://www.google.com/analytics/web/?hl=ru&pli=1#report/visitors-overview/a26895874w20458057p96934174/ 685 | http://business.timesonline.co.uk/article/0,,9065-2473189,00.html 686 | http://example.com/123! 687 | http://example.com/foo--bar 688 | 689 | % some sites have links with trailing dashes 690 | http://www.bloomberg.com/news/articles/2015-06-26/from-deutsche-bank-to-siemens-what-s-troubling-germany-inc- 691 | http://example.com/foo-with-trailing-dash-dot-. 692 | 693 | . 694 | 695 | . 696 | 697 | . 698 | 699 | 700 | . 701 | 702 | 703 | % 704 | % Emails 705 | % 706 | test."foo".bar@gmail.co.uk! 707 | name@example.com 708 | >>name@example.com % markdown blockquote 709 | mailto:name@example.com 710 | MAILTO:NAME@EXAMPLE.COM 711 | mailto:foo_bar@example.com 712 | foo+bar@gmail.com 713 | 192.168.1.1@gmail.com 714 | mailto:foo@bar % explicit protocol make it valid 715 | (foobar email@example.com) 716 | (email@example.com foobar) 717 | (email@example.com) 718 | 719 | % 720 | % International 721 | % 722 | http://✪df.ws/123 723 | http://xn--df-oiy.ws/123 724 | a.ws 725 | ➡.ws/䨹 726 | example.com/䨹 727 | президент.рф 728 | 729 | % Links below provided by diaspora* guys, to make sure regressions will not happen. 730 | % Those left here for historic reasons. 731 | http://www.bürgerentscheid-krankenhäuser.de 732 | http://www.xn--brgerentscheid-krankenhuser-xkc78d.de 733 | http://bündnis-für-krankenhäuser.de/wp-content/uploads/2011/11/cropped-logohp.jpg 734 | http://xn--bndnis-fr-krankenhuser-i5b27cha.de/wp-content/uploads/2011/11/cropped-logohp.jpg 735 | http://ﻡﻮﻘﻋ.ﻭﺯﺍﺭﺓ-ﺍﻼﺘﺻﺍﻼﺗ.ﻢﺻﺭ/ 736 | http://xn--4gbrim.xn----ymcbaaajlc6dj7bxne2c.xn--wgbh1c/ 737 | 738 | 739 | % 740 | % Not links 741 | % 742 | example.invalid 743 | example.invalid/ 744 | http://.example.com 745 | http://-example.com 746 | hppt://example.com 747 | example.coma 748 | -example.coma 749 | foo.123 750 | http://a.b--c.de/ % `--` disabled, because collision possible 751 | localhost % only with protocol allowed 752 | localhost/ 753 | ///localhost % 3 '/' not allowed 754 | ///test.com 755 | //test % Don't allow single level protocol-less domains to avoid false positives 756 | 757 | _http://example.com 758 | _//example.com 759 | _example.com 760 | http://example.com_ 761 | @example.com 762 | 763 | node.js and io.js 764 | 765 | http:// 766 | http://. 767 | http://.. 768 | http://# 769 | http://## 770 | http://? 771 | http://?? 772 | google.com:500000 // invalid port 773 | show image.jpg 774 | path:to:file.pm 775 | /path/to/file.pl 776 | 777 | % 778 | % Not IPv4 779 | % 780 | 1.2.3.4.5 781 | 1.2.3 782 | 1.2.3.400 783 | 1000.2.3.4 784 | a1.2.3.4 785 | 1.2.3.4a 786 | 787 | % 788 | % Not email 789 | % 790 | foo@bar % Should be at second level domain & with correct tld 791 | mailto:bar 792 |

793 | 794 |

Conflict with Angular

795 |
796 | 797 | Should ignore any links in {{...}} 798 | 799 |

800 | {{someVar}} 801 | {{someVar.com.tw}} 802 | {{"http://example.com"}} 803 |

804 |
805 | 806 |

Domain may contain dash

807 |

808 | http://free-group.eu/ 809 | http://free-group.eu:8080/ 810 | http://free-group.eu:8080/?search&follow#id 811 | http://free-group.eu:8080/dash-in-path?search&follow#id 812 |

813 | 814 |

Support IP address

815 |

816 | 110.110.110.110 817 | 12345.124.12.1 818 | 001.000.000.000 819 | 0.0.0.1 820 | 127.0.0.1 821 | 127.0.0.01 822 | 1271.0.0.1 823 | 0.0.0.256 824 | 0.0.0.255 825 |

826 | 827 |

Dealing with wbr tag

828 |

829 | http://time.com/money/3305393/new-taxi-service-is-like-uber-but-for-women-only/ 830 | https://soundcloud.com/uscer/53-girls 831 | https://soundcloud.com/uscer/53-girls 832 |

833 | 834 |

Dealing with parenthesis

835 |

836 | (http://www.example.com/) 837 | (Some text... http://www.example.com/) 838 | http://www.example.com/(Some text...) 839 | http://www.example.com/(Some) 840 | http://en.wikipedia.org/wiki/Darwin_(operating_system) 841 | (http://www.foobar.com/test) 842 | http://www.foobar.com/test). 843 | http://www.asianewsphoto.com/(S(neugxif4twuizg551ywh3f55)) 844 |

845 | 846 |

Unicode issue

847 |

848 | https://github.com/gorhill/uBlock/wiki/Does-µBlock-blocks-ads-or-just-hide-them%3F
849 | http://www.bücher.ch
850 | http://www.example.com/exämple/
851 | http://www.example.com/example/exämple.php?test=täst
852 | http://www.example.com/example/example.php?test=täst
853 |

854 | 855 |

Comma

856 |

857 | http://www.example.com/test,example.html 858 | http://www.example.com/test.html, (comma not to be linkified) 859 | http://www.example.com/test,example.html, (second comma not to be linkified) 860 | http://www.tomshardware.com/reviews/caselabs-ama-recap-jan-2015,4029.html 861 |

862 | 863 |

Blacklist

864 |

865 | http://www.example.com/ 866 |

867 | 868 |

Whitelist

869 |

870 | http://www.example.com/
871 | http://www.example.com/ 872 |

873 | 874 |

Contenteditable

875 |

876 | http://www.example.com/ 877 |

878 | 879 |

Valid characters

880 |

881 | http://example.com/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-.~!$&*+;=:@%/?#(),'[] 882 |

883 | 884 |

Question mark

885 |

886 | is it not foobar.com? 887 |

888 | 889 |

BBCode

890 |

891 | [img]http://example.com/test.png[/img]
892 | [url]http://example.com/test.png[/url]
893 | http://example.com/test.png[b]something-else[/b] 894 |

895 | 896 |

Random tests

897 |

898 | www.vice.news
899 | http://forum.gamer.com.tw/C.php?bsn=12259&snA=264382&tnum=6&subbsn=18
900 | _www.example.com
901 | onenote:#Books&section-id={F1580D31-86DD-4975-9169-CBB0C3846D9D}&page-id={39F02142-9AAA-49C6-AD26-E47114E2BB1C}&end&base-path=https://d.docs.live.net/dc516d79aca53670/OneNote/@Home/Tab9.one
902 | evernote:///view/[userId]/[shardId]/[noteGuid]/[noteGuid]/
903 | magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn
904 | "3.141592653589793238462643383279502884197169399375105820974944592.com" or "yesno.wtf"
905 | 906 | -http://example.com 907 |

908 | 909 |

Bad TLDs

910 |

911 | www.example.free
912 | www.example.zip
913 | www.example.call
914 | www.example.constructor
915 | www.example.onion we actually treat onion as TLD
916 |

917 |
918 | 919 |

920 | 921 | 922 | 923 |

924 |
925 | 926 | 927 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Linkify Plus Plus 7 | 21 | 22 | 23 | 24 | 25 | 26 | 40 | 41 | 42 |
43 |

Linkify Plus Plus Test Page

44 | 45 |
46 |

Random test 1

47 | 48 | Original from http://piro.sakura.ne.jp/xul/textlink/index.html.en#testcases 49 | 50 |

51 | Mozilla(http://www.mozilla.org/) started as a project of next generation www-browser of Netscape(http://www.netscape.com/). See http://www.mozilla.org/src-faq.html#1. Mozilla were planned to be released as "Netscape Communicator 5.0", but the new layout engine "NGLayout" ttp://www.mozilla.org/newlayout/gecko.html prevented it. Netscape 6 ttp://ftp.netscape.com/pub/netscape6/ was released after 2 years from the deciding.And now, the Firefox(h**p://www.mozilla.com/firefox/) web browser is released from the Mozilla Corporation(h++p://www.mozilla.com/). 52 |

53 |

54 | Now, TextLink can parse splitted text nodes as joined texts. Mozilla(http://www.mozilla.org/) started as a project of next generation www-browser of Netscape(http://www.netscape.com/). See http://www.mozilla.org/src-faq.html#1. Mozilla were planned to be released as "Netscape Communicator 5.0", but the new layout engine "NGLayout" ttp://www.mozilla.org/newlayout/gecko.html prevented it. Netscape 6 ttp://ftp.netscape.com/pub/netscape6/ was released after 2 years from the deciding. 55 |

56 |

57 | There are relative links. Firefox www.mozilla.org/products/firefox/ is a new type www-browser and it very extendable. You can find many extensions from update.mozilla.org. I also release some extensions. TBE ./tabextensions/index.html.en and PopupALT _popupalt.html.en are parts of them. (Text Link ignores relative pathes which includes only a filename like "_popupalt.html.en", because strings with the pattern usually indicates only filenames, not relative pathes.) See the parent page ../xul/xul.html to find other extensions in this website. 58 |

59 |

60 | There are links including multi-byte characters. The URI of this website is "http://piro.sakura.ne.jp/", but you can use the secondary URI, "ttp://www98.sakura.ne.jp/ ̄piro/" too. 61 |

62 | Also I don't want these: http://ptt.cc/1234WITHUNICODE, http://example.org/, http://example.org/,http://example.org, 127.0.0.1, http://127.0.0.1, telnet://ptt.cc, AFTERUNICODEhttp://example.org, I'm so happy www, www.sohappywwwhaha, www.... 63 |

64 | 65 |

Random test 2

66 | 67 | Original from http://yellow5.us/firefox/testcases.txt 68 | 69 |

70 | ABOUT 71 | about:blank 72 | about:config 73 | 74 | MAILTO 75 | mailto:example@example.com 76 | 77 | HTTP 78 | http://www.example.com 79 | http://www.example.com. 80 | http://www.example.com/test/ 81 | http://www.example.com/test/. 82 | http://www.example.com/test/index.html 83 | http://www.example.com/test/index.html. 84 | http://user@www.example.com 85 | http://user@www.example.com. 86 | http://user@www.example.com/test/ 87 | http://user@www.example.com/test/. 88 | http://user@www.example.com/test/index.html 89 | http://user@www.example.com/test/index.html. 90 | http://user:password@www.example.com 91 | http://user:password@www.example.com. 92 | http://user:password@www.example.com/test/ 93 | http://user:password@www.example.com/test/. 94 | http://user:password@www.example.com/test/index.html 95 | http://user:password@www.example.com/test/index.html. 96 | http://192.168.0.1 97 | http://192.168.0.1. 98 | http://192.168.0.1/test/ 99 | http://192.168.0.1/test/. 100 | http://192.168.0.1/test/index.html 101 | http://192.168.0.1/test/index.html. 102 | http://user@192.168.0.1 103 | http://user@192.168.0.1. 104 | http://user@192.168.0.1/test/ 105 | http://user@192.168.0.1/test/. 106 | http://user@192.168.0.1/test/index.html 107 | http://user@192.168.0.1/test/index.html. 108 | http://user:password@192.168.0.1 109 | http://user:password@192.168.0.1. 110 | http://user:password@192.168.0.1/test/ 111 | http://user:password@192.168.0.1/test/. 112 | http://user:password@192.168.0.1/test/index.html 113 | http://user:password@192.168.0.1/test/index.html. 114 | 115 | HTTPS 116 | https://www.example.com 117 | https://www.example.com. 118 | https://www.example.com/test/ 119 | https://www.example.com/test/. 120 | https://www.example.com/test/index.html 121 | https://www.example.com/test/index.html. 122 | https://user@www.example.com 123 | https://user@www.example.com. 124 | https://user@www.example.com/test/ 125 | https://user@www.example.com/test/. 126 | https://user@www.example.com/test/index.html 127 | https://user@www.example.com/test/index.html. 128 | https://user:password@www.example.com 129 | https://user:password@www.example.com. 130 | https://user:password@www.example.com/test/ 131 | https://user:password@www.example.com/test/. 132 | https://user:password@www.example.com/test/index.html 133 | https://user:password@www.example.com/test/index.html. 134 | https://192.168.0.1 135 | https://192.168.0.1. 136 | https://192.168.0.1/test/ 137 | https://192.168.0.1/test/. 138 | https://192.168.0.1/test/index.html 139 | https://192.168.0.1/test/index.html. 140 | https://user@192.168.0.1 141 | https://user@192.168.0.1. 142 | https://user@192.168.0.1/test/ 143 | https://user@192.168.0.1/test/. 144 | https://user@192.168.0.1/test/index.html 145 | https://user@192.168.0.1/test/index.html. 146 | https://user:password@192.168.0.1 147 | https://user:password@192.168.0.1. 148 | https://user:password@192.168.0.1/test/ 149 | https://user:password@192.168.0.1/test/. 150 | https://user:password@192.168.0.1/test/index.html 151 | https://user:password@192.168.0.1/test/index.html. 152 | 153 | FTP 154 | ftp://www.example.com 155 | ftp://www.example.com. 156 | ftp://www.example.com/test/ 157 | ftp://www.example.com/test/. 158 | ftp://www.example.com/test/index.html 159 | ftp://www.example.com/test/index.html. 160 | ftp://user@www.example.com 161 | ftp://user@www.example.com. 162 | ftp://user@www.example.com/test/ 163 | ftp://user@www.example.com/test/. 164 | ftp://user@www.example.com/test/index.html 165 | ftp://user@www.example.com/test/index.html. 166 | ftp://user:password@www.example.com 167 | ftp://user:password@www.example.com. 168 | ftp://user:password@www.example.com/test/ 169 | ftp://user:password@www.example.com/test/. 170 | ftp://user:password@www.example.com/test/index.html 171 | ftp://user:password@www.example.com/test/index.html. 172 | ftp://192.168.0.1 173 | ftp://192.168.0.1. 174 | ftp://192.168.0.1/test/ 175 | ftp://192.168.0.1/test/. 176 | ftp://192.168.0.1/test/index.html 177 | ftp://192.168.0.1/test/index.html. 178 | ftp://user@192.168.0.1 179 | ftp://user@192.168.0.1. 180 | ftp://user@192.168.0.1/test/ 181 | ftp://user@192.168.0.1/test/. 182 | ftp://user@192.168.0.1/test/index.html 183 | ftp://user@192.168.0.1/test/index.html. 184 | ftp://user:password@192.168.0.1 185 | ftp://user:password@192.168.0.1. 186 | ftp://user:password@192.168.0.1/test/ 187 | ftp://user:password@192.168.0.1/test/. 188 | ftp://user:password@192.168.0.1/test/index.html 189 | ftp://user:password@192.168.0.1/test/index.html. 190 | 191 | NNTP 192 | nntp://www.example.com 193 | nntp://www.example.com. 194 | nntp://www.example.com/test/ 195 | nntp://www.example.com/test/. 196 | nntp://www.example.com/test/index.html 197 | nntp://www.example.com/test/index.html. 198 | nntp://user@www.example.com 199 | nntp://user@www.example.com. 200 | nntp://user@www.example.com/test/ 201 | nntp://user@www.example.com/test/. 202 | nntp://user@www.example.com/test/index.html 203 | nntp://user@www.example.com/test/index.html. 204 | nntp://user:password@www.example.com 205 | nntp://user:password@www.example.com. 206 | nntp://user:password@www.example.com/test/ 207 | nntp://user:password@www.example.com/test/. 208 | nntp://user:password@www.example.com/test/index.html 209 | nntp://user:password@www.example.com/test/index.html. 210 | nntp://192.168.0.1 211 | nntp://192.168.0.1. 212 | nntp://192.168.0.1/test/ 213 | nntp://192.168.0.1/test/. 214 | nntp://192.168.0.1/test/index.html 215 | nntp://192.168.0.1/test/index.html. 216 | nntp://user@192.168.0.1 217 | nntp://user@192.168.0.1. 218 | nntp://user@192.168.0.1/test/ 219 | nntp://user@192.168.0.1/test/. 220 | nntp://user@192.168.0.1/test/index.html 221 | nntp://user@192.168.0.1/test/index.html. 222 | nntp://user:password@192.168.0.1 223 | nntp://user:password@192.168.0.1. 224 | nntp://user:password@192.168.0.1/test/ 225 | nntp://user:password@192.168.0.1/test/. 226 | nntp://user:password@192.168.0.1/test/index.html 227 | nntp://user:password@192.168.0.1/test/index.html. 228 | 229 | NEWS 230 | news://www.example.com 231 | news://www.example.com. 232 | news://www.example.com/test/ 233 | news://www.example.com/test/. 234 | news://www.example.com/test/index.html 235 | news://www.example.com/test/index.html. 236 | news://user@www.example.com 237 | news://user@www.example.com. 238 | news://user@www.example.com/test/ 239 | news://user@www.example.com/test/. 240 | news://user@www.example.com/test/index.html 241 | news://user@www.example.com/test/index.html. 242 | news://user:password@www.example.com 243 | news://user:password@www.example.com. 244 | news://user:password@www.example.com/test/ 245 | news://user:password@www.example.com/test/. 246 | news://user:password@www.example.com/test/index.html 247 | news://user:password@www.example.com/test/index.html. 248 | news://192.168.0.1 249 | news://192.168.0.1. 250 | news://192.168.0.1/test/ 251 | news://192.168.0.1/test/. 252 | news://192.168.0.1/test/index.html 253 | news://192.168.0.1/test/index.html. 254 | news://user@192.168.0.1 255 | news://user@192.168.0.1. 256 | news://user@192.168.0.1/test/ 257 | news://user@192.168.0.1/test/. 258 | news://user@192.168.0.1/test/index.html 259 | news://user@192.168.0.1/test/index.html. 260 | news://user:password@192.168.0.1 261 | news://user:password@192.168.0.1. 262 | news://user:password@192.168.0.1/test/ 263 | news://user:password@192.168.0.1/test/. 264 | news://user:password@192.168.0.1/test/index.html 265 | news://user:password@192.168.0.1/test/index.html. 266 | 267 | TELNET 268 | telnet://www.example.com 269 | telnet://www.example.com. 270 | telnet://www.example.com/test/ 271 | telnet://www.example.com/test/. 272 | telnet://www.example.com/test/index.html 273 | telnet://www.example.com/test/index.html. 274 | telnet://user@www.example.com 275 | telnet://user@www.example.com. 276 | telnet://user@www.example.com/test/ 277 | telnet://user@www.example.com/test/. 278 | telnet://user@www.example.com/test/index.html 279 | telnet://user@www.example.com/test/index.html. 280 | telnet://user:password@www.example.com 281 | telnet://user:password@www.example.com. 282 | telnet://user:password@www.example.com/test/ 283 | telnet://user:password@www.example.com/test/. 284 | telnet://user:password@www.example.com/test/index.html 285 | telnet://user:password@www.example.com/test/index.html. 286 | telnet://192.168.0.1 287 | telnet://192.168.0.1. 288 | telnet://192.168.0.1/test/ 289 | telnet://192.168.0.1/test/. 290 | telnet://192.168.0.1/test/index.html 291 | telnet://192.168.0.1/test/index.html. 292 | telnet://user@192.168.0.1 293 | telnet://user@192.168.0.1. 294 | telnet://user@192.168.0.1/test/ 295 | telnet://user@192.168.0.1/test/. 296 | telnet://user@192.168.0.1/test/index.html 297 | telnet://user@192.168.0.1/test/index.html. 298 | telnet://user:password@192.168.0.1 299 | telnet://user:password@192.168.0.1. 300 | telnet://user:password@192.168.0.1/test/ 301 | telnet://user:password@192.168.0.1/test/. 302 | telnet://user:password@192.168.0.1/test/index.html 303 | telnet://user:password@192.168.0.1/test/index.html. 304 | 305 | IRC 306 | irc://www.example.com 307 | irc://www.example.com. 308 | irc://www.example.com/test/ 309 | irc://www.example.com/test/. 310 | irc://www.example.com/test/index.html 311 | irc://www.example.com/test/index.html. 312 | irc://user@www.example.com 313 | irc://user@www.example.com. 314 | irc://user@www.example.com/test/ 315 | irc://user@www.example.com/test/. 316 | irc://user@www.example.com/test/index.html 317 | irc://user@www.example.com/test/index.html. 318 | irc://user:password@www.example.com 319 | irc://user:password@www.example.com. 320 | irc://user:password@www.example.com/test/ 321 | irc://user:password@www.example.com/test/. 322 | irc://user:password@www.example.com/test/index.html 323 | irc://user:password@www.example.com/test/index.html. 324 | irc://192.168.0.1 325 | irc://192.168.0.1. 326 | irc://192.168.0.1/test/ 327 | irc://192.168.0.1/test/. 328 | irc://192.168.0.1/test/index.html 329 | irc://192.168.0.1/test/index.html. 330 | irc://user@192.168.0.1 331 | irc://user@192.168.0.1. 332 | irc://user@192.168.0.1/test/ 333 | irc://user@192.168.0.1/test/. 334 | irc://user@192.168.0.1/test/index.html 335 | irc://user@192.168.0.1/test/index.html. 336 | irc://user:password@192.168.0.1 337 | irc://user:password@192.168.0.1. 338 | irc://user:password@192.168.0.1/test/ 339 | irc://user:password@192.168.0.1/test/. 340 | irc://user:password@192.168.0.1/test/index.html 341 | irc://user:password@192.168.0.1/test/index.html. 342 | 343 | CUSTOM 344 | hxxp://www.example.com 345 | hxxp://www.example.com. 346 | hxxp://www.example.com/test/ 347 | hxxp://www.example.com/test/. 348 | hxxp://www.example.com/test/index.html 349 | hxxp://www.example.com/test/index.html. 350 | hxxp://user@www.example.com 351 | hxxp://user@www.example.com. 352 | hxxp://user@www.example.com/test/ 353 | hxxp://user@www.example.com/test/. 354 | hxxp://user@www.example.com/test/index.html 355 | hxxp://user@www.example.com/test/index.html. 356 | hxxp://user:password@www.example.com 357 | hxxp://user:password@www.example.com. 358 | hxxp://user:password@www.example.com/test/ 359 | hxxp://user:password@www.example.com/test/. 360 | hxxp://user:password@www.example.com/test/index.html 361 | hxxp://user:password@www.example.com/test/index.html. 362 | hxxp://192.168.0.1 363 | hxxp://192.168.0.1. 364 | hxxp://192.168.0.1/test/ 365 | hxxp://192.168.0.1/test/. 366 | hxxp://192.168.0.1/test/index.html 367 | hxxp://192.168.0.1/test/index.html. 368 | hxxp://user@192.168.0.1 369 | hxxp://user@192.168.0.1. 370 | hxxp://user@192.168.0.1/test/ 371 | hxxp://user@192.168.0.1/test/. 372 | hxxp://user@192.168.0.1/test/index.html 373 | hxxp://user@192.168.0.1/test/index.html. 374 | hxxp://user:password@192.168.0.1 375 | hxxp://user:password@192.168.0.1. 376 | hxxp://user:password@192.168.0.1/test/ 377 | hxxp://user:password@192.168.0.1/test/. 378 | hxxp://user:password@192.168.0.1/test/index.html 379 | hxxp://user:password@192.168.0.1/test/index.html. 380 | 381 | CUSTOM 382 | h**p://www.example.com 383 | h**p://www.example.com. 384 | h**p://www.example.com/test/ 385 | h**p://www.example.com/test/. 386 | h**p://www.example.com/test/index.html 387 | h**p://www.example.com/test/index.html. 388 | h**p://user@www.example.com 389 | h**p://user@www.example.com. 390 | h**p://user@www.example.com/test/ 391 | h**p://user@www.example.com/test/. 392 | h**p://user@www.example.com/test/index.html 393 | h**p://user@www.example.com/test/index.html. 394 | h**p://user:password@www.example.com 395 | h**p://user:password@www.example.com. 396 | h**p://user:password@www.example.com/test/ 397 | h**p://user:password@www.example.com/test/. 398 | h**p://user:password@www.example.com/test/index.html 399 | h**p://user:password@www.example.com/test/index.html. 400 | h**p://192.168.0.1 401 | h**p://192.168.0.1. 402 | h**p://192.168.0.1/test/ 403 | h**p://192.168.0.1/test/. 404 | h**p://192.168.0.1/test/index.html 405 | h**p://192.168.0.1/test/index.html. 406 | h**p://user@192.168.0.1 407 | h**p://user@192.168.0.1. 408 | h**p://user@192.168.0.1/test/ 409 | h**p://user@192.168.0.1/test/. 410 | h**p://user@192.168.0.1/test/index.html 411 | h**p://user@192.168.0.1/test/index.html. 412 | h**p://user:password@192.168.0.1 413 | h**p://user:password@192.168.0.1. 414 | h**p://user:password@192.168.0.1/test/ 415 | h**p://user:password@192.168.0.1/test/. 416 | h**p://user:password@192.168.0.1/test/index.html 417 | h**p://user:password@192.168.0.1/test/index.html. 418 | 419 | WWW (no protocol) 420 | www.example.com 421 | www.example.com. 422 | www.example.com/test/ 423 | www.example.com/test/. 424 | www.example.com/test/index.html 425 | www.example.com/test/index.html. 426 | user@www.example.com (ambiguous, but recognized subdomain. not an e-mail address) 427 | user@www.example.com. (ambiguous, but recognized subdomain. not an e-mail address) 428 | user@www.example.com/test/ 429 | user@www.example.com/test/. 430 | user@www.example.com/test/index.html 431 | user@www.example.com/test/index.html. 432 | user:password@www.example.com 433 | user:password@www.example.com. 434 | user:password@www.example.com/test/ 435 | user:password@www.example.com/test/. 436 | user:password@www.example.com/test/index.html 437 | user:password@www.example.com/test/index.html. 438 | 439 | FTP (no protocol) 440 | ftp.example.com 441 | ftp.example.com. 442 | ftp.example.com/test/ 443 | ftp.example.com/test/. 444 | ftp.example.com/test/index.html 445 | ftp.example.com/test/index.html. 446 | user@ftp.example.com (ambiguous, but recognized subdomain. not an e-mail address) 447 | user@ftp.example.com. (ambiguous, but recognized subdomain. not an e-mail address) 448 | user@ftp.example.com/test/ 449 | user@ftp.example.com/test/. 450 | user@ftp.example.com/test/index.html 451 | user@ftp.example.com/test/index.html. 452 | user:password@ftp.example.com 453 | user:password@ftp.example.com. 454 | user:password@ftp.example.com/test/ 455 | user:password@ftp.example.com/test/. 456 | user:password@ftp.example.com/test/index.html 457 | user:password@ftp.example.com/test/index.html. 458 | 459 | IRC (no protocol) 460 | irc.example.com 461 | irc.example.com. 462 | irc.example.com/test/ 463 | irc.example.com/test/. 464 | irc.example.com/test/index.html 465 | irc.example.com/test/index.html. 466 | user@irc.example.com (ambiguous, but recognized subdomain. not an e-mail address) 467 | user@irc.example.com. (ambiguous, but recognized subdomain. not an e-mail address) 468 | user@irc.example.com/test/ 469 | user@irc.example.com/test/. 470 | user@irc.example.com/test/index.html 471 | user@irc.example.com/test/index.html. 472 | user:password@irc.example.com 473 | user:password@irc.example.com. 474 | user:password@irc.example.com/test/ 475 | user:password@irc.example.com/test/. 476 | user:password@irc.example.com/test/index.html 477 | user:password@irc.example.com/test/index.html. 478 | #test-name@irc.example.com 479 | #test-name@irc.example.com. 480 | irc.example.com#test-name 481 | irc.example.com#test-name. 482 | 483 | IP (no protocol) 484 | 192.168.0.1 (not linkified; pattern too common) 485 | 192.168.0.1. (not linkified; pattern too common) 486 | 192.168.0.1/test/ 487 | 192.168.0.1/test/. 488 | 192.168.0.1/test/index.html 489 | 192.168.0.1/test/index.html. 490 | user@192.168.0.1 (ambiguous; should be recognized as e-mail) 491 | user@192.168.0.1. (ambiguous; should be recognized as e-mail) 492 | user@192.168.0.1/test/ 493 | user@192.168.0.1/test/. 494 | user@192.168.0.1/test/index.html 495 | user@192.168.0.1/test/index.html. 496 | user:password@192.168.0.1 497 | user:password@192.168.0.1. 498 | user:password@192.168.0.1/test/ 499 | user:password@192.168.0.1/test/. 500 | user:password@192.168.0.1/test/index.html 501 | user:password@192.168.0.1/test/index.html. 502 | 503 | OTHER (no protocol) 504 | subdomain.example.com (not linkified; pattern too common) 505 | subdomain.example.com. (not linkified; pattern too common) 506 | subdomain.example.com/test/ 507 | subdomain.example.com/test/. 508 | subdomain.example.com/test/index.html 509 | subdomain.example.com/test/index.html. 510 | user@subdomain.example.com (ambiguous; should be recognized as e-mail) 511 | user@subdomain.example.com. (ambiguous; should be recognized as e-mail) 512 | user@subdomain.example.com/test/ 513 | user@subdomain.example.com/test/. 514 | user@subdomain.example.com/test/index.html 515 | user@subdomain.example.com/test/index.html. 516 | user:password@subdomain.example.com 517 | user:password@subdomain.example.com. 518 | user:password@subdomain.example.com/test/ 519 | user:password@subdomain.example.com/test/. 520 | user:password@subdomain.example.com/test/index.html 521 | user:password@subdomain.example.com/test/index.html. 522 | 523 | EMAIL 524 | test@example.com 525 | test@example.com. 526 | test.test@test.example.com 527 | test.test@test.example.com. 528 | test@192.168.0.1 529 | test@192.168.0.1. 530 | test.test@192.168.0.1 531 | test.test@192.168.0.1. 532 | 533 | IMAGE 534 | http://www.example.com/image.jpg 535 | http://www.example.com/image.jpeg 536 | http://www.example.com/image.png 537 | http://www.example.com/image.gif 538 | http://www.example.com/image.bmp 539 | http://www.example.com/image.jpg.test (not an image) 540 | http://www.example.com/image.jpeg.test (not an image) 541 | http://www.example.com/image.png.test (not an image) 542 | http://www.example.com/image.gif.test (not an image) 543 | http://www.example.com/image.bmp.test (not an image) 544 | http://www.example.com/image.jpg?test 545 | http://www.example.com/image.jpeg?test 546 | http://www.example.com/image.png?test 547 | http://www.example.com/image.gif?test 548 | http://www.example.com/image.bmp?test 549 | http://www.example.com/image.test?jpg (not an image) 550 | http://www.example.com/image.test?jpeg (not an image) 551 | http://www.example.com/image.test?png (not an image) 552 | http://www.example.com/image.test?gif (not an image) 553 | http://www.example.com/image.test?bmp (not an image) 554 | http://www.example.com/image.jpg#test 555 | http://www.example.com/image.jpeg#test 556 | http://www.example.com/image.png#test 557 | http://www.example.com/image.gif#test 558 | http://www.example.com/image.bmp#test 559 | http://www.example.com/image.test#jpg (not an image) 560 | http://www.example.com/image.test#jpeg (not an image) 561 | http://www.example.com/image.test#png (not an image) 562 | http://www.example.com/image.test#gif (not an image) 563 | http://www.example.com/image.test#bmp (not an image) 564 | https://greasyfork.org/assets/blacklogo96-0596aff6108f83c3073764496d7768ec.png 565 | http://i.imgur.com/25zhGbg.jpg 566 | https://f061172b00c7bca1e36fdd56f00f238cf2545831.googledrive.com/host/0B_P4A1paVEPbb1UxSUdua3Fwc1k/Vorago_chathead.png 567 | https://secure.runescape.com/m=weblogin/logout.ws?.png 568 | 569 | Dots 570 | http://www.example.com 571 | http://www.example.com. 572 | http://www.example.com.. 573 | http://www.example.com... 574 | http://www.example.com/ 575 | http://www.example.com/. 576 | http://www.example.com/.. 577 | http://www.example.com/... 578 | http://www.example.com/ 579 | http://www.example.com/. 580 | http://www.example.com/./. 581 | http://www.example.com/../. 582 |

583 | 584 |

Random test 3

585 | 586 | Original from http://markdown-it.github.io/linkify-it/, licensed under MIT 587 | 588 |

589 | % 590 | % Regular links 591 | % 592 | My http://example.com site 593 | My http://example.com/ site 594 | http://example.com/foo_bar/ 595 | http://user:pass@example.com:8080 596 | http://user@example.com 597 | http://user@example.com:8080 598 | http://user:pass@example.com 599 | [https](https://www.ibm.com)[mailto](mailto:someone@ibm.com) % should not catch as auth (before @ in big link) 600 | http://example.com:8080 601 | http://example.com/?foo=bar 602 | http://example.com?foo=bar 603 | http://example.com/#foo=bar 604 | http://example.com#foo=bar 605 | http://a.in 606 | HTTP://GOOGLE.COM 607 | http://example.invalid % don't restrict root domain when schema exists 608 | http://inrgess2 % Allow local domains to end with digit 609 | http://999 % ..and start with digit, and have digits only 610 | http://host-name % local domain with dash 611 | >>example.com % markdown blockquote 612 | >>http://example.com % markdown blockquote 613 | http://lyricstranslate.com/en/someone-you-നിന്നെ-പോലൊരാള്‍.html % With control character 614 | 615 | % 616 | % localhost (only with protocol allowed) 617 | % 618 | //localhost 619 | //test.123 620 | http://localhost:8000? 621 | 622 | % 623 | % Other protocols 624 | % 625 | My ssl https://example.com site 626 | My ftp://example.com site 627 | 628 | % 629 | % Neutral proto 630 | % 631 | My ssl //example.com site 632 | 633 | % 634 | % IPs 635 | % 636 | 4.4.4.4 637 | 192.168.1.1/abc 638 | 639 | % 640 | % Fuzzy 641 | % 642 | test.example@http://vk.com 643 | text:http://example.com/ 644 | google.com 645 | google.com: // no port 646 | s.l.o.w.io 647 | a-b.com 648 | GOOGLE.COM. 649 | google.xxx // known tld 650 | 651 | % 652 | % Correct termination for . , ! ? [] {} () "" '' 653 | % 654 | (Scoped http://example.com/foo_bar) 655 | http://example.com/foo_bar_(wiki) 656 | http://foo.com/blah_blah_[other] 657 | http://foo.com/blah_blah_{I'm_king} 658 | http://foo.com/blah_blah_I'm_king 659 | http://www.kmart.com/bestway-10'-x-30inch-steel-pro-frame-pool/p-004W007538417001P 660 | http://foo.com/blah_blah_"doublequoted" 661 | http://foo.com/blah_blah_'singlequoted' 662 | (Scoped like http://example.com/foo_bar) 663 | [Scoped like http://example.com/foo_bar] 664 | {Scoped like http://example.com/foo_bar} 665 | "Quoted like http://example.com/foo_bar" 666 | 'Quoted like http://example.com/foo_bar' 667 | [example.com/foo_bar.jpg)] 668 | http://example.com/foo_bar.jpg. 669 | http://example.com/foo_bar/. 670 | http://example.com/foo_bar, 671 | https://github.com/markdown-it/linkify-it/compare/360b13a733f521a8d4903d3a5e1e46c357e9d3ce...f580766349525150a80a32987bb47c2d592efc33 672 | http://example.com/foo_bar... 673 | http://172.26.142.48/viewerjs/#../0529/slides.pdf 674 | http://example.com/foo_bar.. 675 | http://example.com/foo_bar?p=10. 676 | https://www.google.ru/maps/@59.9393895,30.3165389,15z?hl=ru 677 | https://www.google.com/maps/place/New+York,+NY,+USA/@40.702271,-73.9968471,11z/data=!4m2!3m1!1s0x89c24fa5d33f083b:0xc80b8f06e177fe62?hl=en 678 | https://www.google.com/analytics/web/?hl=ru&pli=1#report/visitors-overview/a26895874w20458057p96934174/ 679 | http://business.timesonline.co.uk/article/0,,9065-2473189,00.html 680 | http://example.com/123! 681 | http://example.com/foo--bar 682 | 683 | % some sites have links with trailing dashes 684 | http://www.bloomberg.com/news/articles/2015-06-26/from-deutsche-bank-to-siemens-what-s-troubling-germany-inc- 685 | http://example.com/foo-with-trailing-dash-dot-. 686 | 687 | . 688 | 689 | . 690 | 691 | . 692 | 693 | 694 | . 695 | 696 | 697 | % 698 | % Emails 699 | % 700 | test."foo".bar@gmail.co.uk! 701 | name@example.com 702 | >>name@example.com % markdown blockquote 703 | mailto:name@example.com 704 | MAILTO:NAME@EXAMPLE.COM 705 | mailto:foo_bar@example.com 706 | foo+bar@gmail.com 707 | 192.168.1.1@gmail.com 708 | mailto:foo@bar % explicit protocol make it valid 709 | (foobar email@example.com) 710 | (email@example.com foobar) 711 | (email@example.com) 712 | 713 | % 714 | % International 715 | % 716 | http://✪df.ws/123 717 | http://xn--df-oiy.ws/123 718 | a.ws 719 | ➡.ws/䨹 720 | example.com/䨹 721 | президент.рф 722 | 723 | % Links below provided by diaspora* guys, to make sure regressions will not happen. 724 | % Those left here for historic reasons. 725 | http://www.bürgerentscheid-krankenhäuser.de 726 | http://www.xn--brgerentscheid-krankenhuser-xkc78d.de 727 | http://bündnis-für-krankenhäuser.de/wp-content/uploads/2011/11/cropped-logohp.jpg 728 | http://xn--bndnis-fr-krankenhuser-i5b27cha.de/wp-content/uploads/2011/11/cropped-logohp.jpg 729 | http://ﻡﻮﻘﻋ.ﻭﺯﺍﺭﺓ-ﺍﻼﺘﺻﺍﻼﺗ.ﻢﺻﺭ/ 730 | http://xn--4gbrim.xn----ymcbaaajlc6dj7bxne2c.xn--wgbh1c/ 731 | 732 | 733 | % 734 | % Not links 735 | % 736 | example.invalid 737 | example.invalid/ 738 | http://.example.com 739 | http://-example.com 740 | hppt://example.com 741 | example.coma 742 | -example.coma 743 | foo.123 744 | http://a.b--c.de/ % `--` disabled, because collision possible 745 | localhost % only with protocol allowed 746 | localhost/ 747 | ///localhost % 3 '/' not allowed 748 | ///test.com 749 | //test % Don't allow single level protocol-less domains to avoid false positives 750 | 751 | _http://example.com 752 | _//example.com 753 | _example.com 754 | http://example.com_ 755 | @example.com 756 | 757 | node.js and io.js 758 | 759 | http:// 760 | http://. 761 | http://.. 762 | http://# 763 | http://## 764 | http://? 765 | http://?? 766 | google.com:500000 // invalid port 767 | show image.jpg 768 | path:to:file.pm 769 | /path/to/file.pl 770 | 771 | % 772 | % Not IPv4 773 | % 774 | 1.2.3.4.5 775 | 1.2.3 776 | 1.2.3.400 777 | 1000.2.3.4 778 | a1.2.3.4 779 | 1.2.3.4a 780 | 781 | % 782 | % Not email 783 | % 784 | foo@bar % Should be at second level domain & with correct tld 785 | mailto:bar 786 |

787 | 788 |

Conflict with Angular

789 |
790 | 791 | Should ignore any links in {{...}} 792 | 793 |

794 | {{someVar}} 795 | {{someVar.com.tw}} 796 | {{"http://example.com"}} 797 |

798 |
799 | 800 |

Domain may contain dash

801 |

802 | http://free-group.eu/ 803 | http://free-group.eu:8080/ 804 | http://free-group.eu:8080/?search&follow#id 805 | http://free-group.eu:8080/dash-in-path?search&follow#id 806 |

807 | 808 |

Support IP address

809 |

810 | 110.110.110.110 811 | 12345.124.12.1 812 | 001.000.000.000 813 | 0.0.0.1 814 | 127.0.0.1 815 | 127.0.0.01 816 | 1271.0.0.1 817 | 0.0.0.256 818 | 0.0.0.255 819 |

820 | 821 |

Dealing with wbr tag

822 |

823 | http://time.com/money/3305393/new-taxi-service-is-like-uber-but-for-women-only/ 824 | https://soundcloud.com/uscer/53-girls 825 | https://soundcloud.com/uscer/53-girls 826 |

827 | 828 |

Dealing with parenthesis

829 |

830 | (http://www.example.com/) 831 | (Some text... http://www.example.com/) 832 | http://www.example.com/(Some text...) 833 | http://www.example.com/(Some) 834 | http://en.wikipedia.org/wiki/Darwin_(operating_system) 835 | (http://www.foobar.com/test) 836 | http://www.foobar.com/test). 837 | http://www.asianewsphoto.com/(S(neugxif4twuizg551ywh3f55)) 838 |

839 | 840 |

Unicode issue

841 |

842 | https://github.com/gorhill/uBlock/wiki/Does-µBlock-blocks-ads-or-just-hide-them%3F
843 | http://www.bücher.ch
844 | http://www.example.com/exämple/
845 | http://www.example.com/example/exämple.php?test=täst
846 | http://www.example.com/example/example.php?test=täst
847 |

848 | 849 |

Comma

850 |

851 | http://www.example.com/test,example.html 852 | http://www.example.com/test.html, (comma not to be linkified) 853 | http://www.example.com/test,example.html, (second comma not to be linkified) 854 | http://www.tomshardware.com/reviews/caselabs-ama-recap-jan-2015,4029.html 855 |

856 | 857 |

Blacklist

858 |

859 | http://www.example.com/ 860 |

861 | 862 |

Whitelist

863 |

864 | http://www.example.com/
865 | http://www.example.com/ 866 |

867 | 868 |

Contenteditable

869 |

870 | http://www.example.com/ 871 |

872 | 873 |

Valid characters

874 |

875 | http://example.com/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-.~!$&*+;=:@%/?#(),'[] 876 |

877 | 878 |

Question mark

879 |

880 | is it not foobar.com? 881 |

882 | 883 |

BBCode

884 |

885 | [img]http://example.com/test.png[/img]
886 | [url]http://example.com/test.png[/url]
887 | http://example.com/test.png[b]something-else[/b] 888 |

889 | 890 |

Random tests

891 |

892 | www.vice.news
893 | http://forum.gamer.com.tw/C.php?bsn=12259&snA=264382&tnum=6&subbsn=18
894 | _www.example.com
895 | onenote:#Books&section-id={F1580D31-86DD-4975-9169-CBB0C3846D9D}&page-id={39F02142-9AAA-49C6-AD26-E47114E2BB1C}&end&base-path=https://d.docs.live.net/dc516d79aca53670/OneNote/@Home/Tab9.one
896 | evernote:///view/[userId]/[shardId]/[noteGuid]/[noteGuid]/
897 | magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn
898 | "3.141592653589793238462643383279502884197169399375105820974944592.com" or "yesno.wtf"
899 | 900 | -http://example.com 901 |

902 | 903 |

Bad TLDs

904 |

905 | www.example.free
906 | www.example.zip
907 | www.example.call
908 | www.example.constructor
909 |

910 |
911 | 912 |

913 | 914 | 915 |

916 |
917 | 918 | 919 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | 4 | export default [ 5 | { 6 | ignores: ["dist", "dist-extension"] 7 | }, 8 | js.configs.recommended, 9 | { 10 | "rules": { 11 | "dot-notation": 2, 12 | "max-statements-per-line": 2, 13 | }, 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node 18 | } 19 | } 20 | } 21 | ]; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkify-plus-plus", 3 | "description": "Based on Linkify Plus. Turn plain text URLs into links.", 4 | "version": "12.0.1", 5 | "repository": "eight04/linkify-plus-plus", 6 | "license": "BSD-3-Clause", 7 | "author": "eight04 ", 8 | "devDependencies": { 9 | "@rollup/plugin-inject": "^5.0.5", 10 | "@rollup/plugin-json": "^6.1.0", 11 | "@rollup/plugin-node-resolve": "^16.0.0", 12 | "@rollup/plugin-terser": "^0.4.4", 13 | "dataurl": "^0.1.0", 14 | "eslint": "^9.20.1", 15 | "rollup": "^4.34.8", 16 | "rollup-plugin-cjs-es": "^3.0.0", 17 | "rollup-plugin-copy-glob": "^0.4.1", 18 | "rollup-plugin-iife": "^0.7.1", 19 | "rollup-plugin-write-output": "^0.2.1", 20 | "shx": "^0.3.4", 21 | "sync-version": "^1.0.1", 22 | "tiny-glob": "^0.2.9", 23 | "userscript-meta-cli": "^0.4.2", 24 | "web-ext": "^8.4.0" 25 | }, 26 | "scripts": { 27 | "build": "sync-version src/static/manifest.json && shx rm -rf dist-extension/* && rollup -c && web-ext build", 28 | "build-dev": "rollup -cw", 29 | "build-git": "git archive --output web-ext-artifacts/source.zip master", 30 | "test": "eslint . --cache && web-ext lint", 31 | "preversion": "npm test", 32 | "version": "npm run build && git add .", 33 | "postversion": "git push --follow-tags && npm run build-git", 34 | "start": "web-ext run" 35 | }, 36 | "userscript": { 37 | "name": "Linkify Plus Plus", 38 | "namespace": "eight04.blogspot.com", 39 | "include": "*", 40 | "exclude": [ 41 | "https://www.google.*/search*", 42 | "https://www.google.*/webhp*", 43 | "https://music.google.com/*", 44 | "https://mail.google.com/*", 45 | "https://docs.google.com/*", 46 | "https://encrypted.google.com/*", 47 | "https://*101weiqi.com/*", 48 | "https://w3c*.github.io/*", 49 | "https://www.paypal.com/*", 50 | "https://term.ptt.cc/*", 51 | "https://mastodon.social/*" 52 | ], 53 | "grant": [ 54 | "GM.getValue", 55 | "GM.setValue", 56 | "GM.deleteValue", 57 | "GM_addStyle", 58 | "GM_registerMenuCommand", 59 | "GM_getValue", 60 | "GM_setValue", 61 | "GM_deleteValue", 62 | "GM_addValueChangeListener", 63 | "unsafeWindow" 64 | ], 65 | "compatible": [ 66 | "firefox Tampermonkey latest", 67 | "chrome Tampermonkey latest" 68 | ] 69 | }, 70 | "private": true, 71 | "dependencies": { 72 | "event-lite": "^1.0.0", 73 | "gm-webext-pref": "^0.4.2", 74 | "linkify-plus-plus-core": "^0.7.0", 75 | "sentinel-js": "^0.0.7", 76 | "webext-dialog": "^0.1.1", 77 | "webext-pref": "^0.6.0", 78 | "webextension-polyfill": "^0.12.0" 79 | }, 80 | "webExt": { 81 | "sourceDir": "dist-extension", 82 | "build": { 83 | "overwriteDest": true 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dataurl from "dataurl"; 2 | import fs from "fs"; 3 | import usm from "userscript-meta-cli"; 4 | import glob from "tiny-glob"; 5 | 6 | import copy from 'rollup-plugin-copy-glob'; 7 | import cjs from "rollup-plugin-cjs-es"; 8 | import iife from "rollup-plugin-iife"; 9 | import json from "@rollup/plugin-json"; 10 | import output from "rollup-plugin-write-output"; 11 | import terser from "@rollup/plugin-terser"; 12 | import resolve from "@rollup/plugin-node-resolve"; 13 | import inject from "@rollup/plugin-inject"; 14 | 15 | const DEV = process.env.ROLLUP_WATCH; 16 | 17 | function commonPlugins(cache) { 18 | return [ 19 | resolve({ 20 | dedupe: ["event-lite"] 21 | }), 22 | json(), 23 | cjs({nested: true, cache}) 24 | ]; 25 | } 26 | 27 | export default async () => [ 28 | { 29 | input: await glob("src/extension/*.js"), 30 | output: { 31 | format: "es", 32 | dir: "dist-extension/js" 33 | }, 34 | plugins: [ 35 | copy([ 36 | { 37 | files: "src/static/**/*", 38 | dest: "dist-extension" 39 | } 40 | ]), 41 | ...commonPlugins(), 42 | inject({ 43 | exclude: ["**/*/browser-polyfill.js"], 44 | browser: "webextension-polyfill" 45 | }), 46 | iife(), 47 | output([ 48 | { 49 | test: /(options|dialog)\.js$/, 50 | target: "dist-extension/$1.html", 51 | handle: (content, {htmlScripts}) => content.replace(/.*<\/body>/, `${htmlScripts}`) 52 | }, 53 | { 54 | test: /background\.js$/, 55 | target: "dist-extension/manifest.json", 56 | handle: (content, {scripts}) => { 57 | content.background.scripts = scripts; 58 | return content; 59 | } 60 | }, 61 | { 62 | test: /content\.js$/, 63 | target: "dist-extension/manifest.json", 64 | handle: (content, {scripts}) => { 65 | content.content_scripts[0].js = scripts; 66 | content.content_scripts[0].exclude_globs = usm.getMeta().exclude; 67 | return content; 68 | } 69 | } 70 | ]), 71 | !DEV && terser({module: false}) 72 | ] 73 | }, 74 | { 75 | input: { 76 | "linkify-plus-plus.user": "src/userscript/index.js" 77 | }, 78 | output: { 79 | format: "es", 80 | banner: metaDataBlock(), 81 | dir: "dist" 82 | }, 83 | plugins: [ 84 | cleanMessages(), 85 | ...commonPlugins(false), 86 | ] 87 | } 88 | ]; 89 | 90 | function metaDataBlock() { 91 | const meta = usm.getMeta(); 92 | meta.icon = dataurl.format({ 93 | data: fs.readFileSync("src/static/icon.svg"), 94 | mimetype: "image/svg+xml" 95 | }); 96 | return usm.stringify(meta); 97 | } 98 | 99 | function cleanMessages() { 100 | return { 101 | name: "clean-messages", 102 | transform: (code, id) => { 103 | if (!/messages.json/.test(id)) return; 104 | 105 | const message = JSON.parse(code); 106 | const newMessage = {}; 107 | for (const [key, value] of Object.entries(message)) { 108 | if (value.placeholders) { 109 | value.message = value.message.replace(/\$\w+\$/g, m => { 110 | const name = m.slice(1, -1).toLowerCase(); 111 | return value.placeholders[name].content; 112 | }); 113 | } 114 | if (/^options/.test(key)) { 115 | newMessage[key] = value.message; 116 | } else if (/^pref/.test(key)) { 117 | newMessage[key[4].toLowerCase() + key.slice(5)] = value.message; 118 | } 119 | } 120 | return JSON.stringify(newMessage, null, 2); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/extension/background.js: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill" 2 | 3 | let domain = ""; 4 | const ports = new Set; 5 | 6 | browser.runtime.onConnect.addListener(port => { 7 | if (port.name !== "optionsPage" || port.error) { 8 | return; 9 | } 10 | ports.add(port); 11 | port.onDisconnect.addListener(() => ports.delete(port)); 12 | port.postMessage({method: "domainChange", domain}); 13 | }); 14 | 15 | browser.browserAction.onClicked.addListener(tab => { 16 | const url = new URL(tab.url); 17 | domain = url.hostname; 18 | for (const port of ports) { 19 | port.postMessage({method: "domainChange", domain}); 20 | } 21 | // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1949504 22 | browser.runtime.openOptionsPage(); 23 | }); 24 | 25 | if (/Chrome\/\d+/.test(navigator.userAgent)) { 26 | browser.browserAction.setIcon({ 27 | path: "/icon.svg" 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/extension/content.js: -------------------------------------------------------------------------------- 1 | const {startLinkifyPlusPlus} = require("../lib/main"); 2 | const pref = require("../lib/extension-pref"); 3 | 4 | startLinkifyPlusPlus(async () => { 5 | await pref.ready; 6 | await pref.setCurrentScope(location.hostname); 7 | return pref; 8 | }); 9 | -------------------------------------------------------------------------------- /src/extension/dialog.js: -------------------------------------------------------------------------------- 1 | require("webext-dialog/popup"); 2 | -------------------------------------------------------------------------------- /src/extension/options.js: -------------------------------------------------------------------------------- 1 | const browser = require("webextension-polyfill"); 2 | const {createUI, createBinding} = require("webext-pref-ui"); 3 | const {createDialogService} = require("webext-dialog"); 4 | 5 | const prefBody = require("../lib/pref-body"); 6 | const pref = require("../lib/extension-pref"); 7 | 8 | const dialog = createDialogService({ 9 | path: "dialog.html", 10 | getMessage: key => browser.i18n.getMessage(`dialog${cap(key)}`) 11 | }); 12 | 13 | function cap(s) { 14 | return s[0].toUpperCase() + s.slice(1); 15 | } 16 | 17 | pref.ready.then(() => { 18 | let domain = ""; 19 | 20 | const root = document.querySelector(".pref-root"); 21 | 22 | const getMessage = (key, params) => browser.i18n.getMessage(`pref${cap(key)}`, params); 23 | 24 | root.append(createUI({ 25 | body: prefBody(browser.i18n.getMessage), 26 | getMessage 27 | })); 28 | 29 | createBinding({ 30 | pref, 31 | root, 32 | getNewScope: () => domain, 33 | getMessage, 34 | alert: dialog.alert, 35 | confirm: dialog.confirm, 36 | prompt: dialog.prompt 37 | }); 38 | 39 | const port = browser.runtime.connect({ 40 | name: "optionsPage" 41 | }); 42 | port.onMessage.addListener(message => { 43 | if (message.method === "domainChange") { 44 | domain = message.domain; 45 | pref.setCurrentScope(pref.getScopeList().includes(domain) ? domain : "global"); 46 | } 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/lib/extension-pref.js: -------------------------------------------------------------------------------- 1 | const {createPref, createWebextStorage} = require("webext-pref"); 2 | const prefDefault = require("./pref-default"); 3 | 4 | const pref = createPref(prefDefault()); 5 | pref.ready = pref.connect(createWebextStorage()); 6 | 7 | module.exports = pref; 8 | -------------------------------------------------------------------------------- /src/lib/main.mjs: -------------------------------------------------------------------------------- 1 | import {UrlMatcher, INVALID_TAGS} from "linkify-plus-plus-core"; 2 | 3 | import triggers from "./triggers/index.mjs"; 4 | import {processedNodes} from "./triggers/cache.mjs"; 5 | 6 | function createValidator({includeElement, excludeElement}) { 7 | const f = function(node) { 8 | if (processedNodes.has(node)) { 9 | return false; 10 | } 11 | 12 | if (node.isContentEditable) { 13 | return false; 14 | } 15 | 16 | if (node.matches) { 17 | if (includeElement && node.matches(includeElement)) { 18 | return true; 19 | } 20 | if (excludeElement && node.matches(excludeElement)) { 21 | return false; 22 | } 23 | } 24 | return true; 25 | }; 26 | f.isIncluded = node => { 27 | return includeElement && node.matches(includeElement); 28 | }; 29 | f.isExcluded = node => { 30 | if (INVALID_TAGS[node.localName]) { 31 | return true; 32 | } 33 | return excludeElement && node.matches(excludeElement); 34 | }; 35 | return f; 36 | } 37 | 38 | function stringToList(value) { 39 | value = value.trim(); 40 | if (!value) { 41 | return []; 42 | } 43 | return value.split(/\s*\n\s*/g); 44 | } 45 | 46 | function createOptions(pref) { 47 | const options = {}; 48 | pref.on("change", update); 49 | update(pref.getAll()); 50 | return options; 51 | 52 | function update(changes) { 53 | Object.assign(options, changes); 54 | options.validator = createValidator(options); 55 | if (typeof options.customRules === "string") { 56 | options.customRules = stringToList(options.customRules); 57 | } 58 | options.matcher = new UrlMatcher(options); 59 | options.onlink = options.embedImageExcludeElement ? onlink : null; 60 | } 61 | 62 | function onlink({link, range, content}) { 63 | if (link.childNodes[0].localName !== "img" || !options.embedImageExcludeElement) { 64 | return; 65 | } 66 | 67 | var parent = range.startContainer; 68 | // it might be a text node 69 | if (!parent.closest) { 70 | parent = parent.parentNode; 71 | } 72 | if (!parent.closest(options.embedImageExcludeElement)) return; 73 | // remove image 74 | link.innerHTML = ""; 75 | link.appendChild(content); 76 | } 77 | } 78 | 79 | export async function startLinkifyPlusPlus(getPref) { 80 | // Limit contentType to specific content type 81 | if ( 82 | document.contentType && 83 | !["text/plain", "text/html", "application/xhtml+xml"].includes(document.contentType) 84 | ) { 85 | return; 86 | } 87 | 88 | const pref = await getPref(); 89 | const options = createOptions(pref); 90 | for (const trigger of triggers) { 91 | if (pref.get(trigger.key)) { 92 | trigger.enable(options); 93 | } 94 | } 95 | pref.on("change", changes => { 96 | for (const trigger of triggers) { 97 | if (changes[trigger.key] === true) { 98 | trigger.enable(options); 99 | } 100 | if (changes[trigger.key] === false) { 101 | trigger.disable(); 102 | } 103 | } 104 | }); 105 | } 106 | 107 | -------------------------------------------------------------------------------- /src/lib/pref-body.js: -------------------------------------------------------------------------------- 1 | module.exports = getMessage => { 2 | return [ 3 | { 4 | type: "section", 5 | label: getMessage("optionsUrlMatcherLabel"), 6 | children: [ 7 | { 8 | key: "fuzzyIp", 9 | type: "checkbox", 10 | label: getMessage("optionsFuzzyIpLabel") 11 | }, 12 | { 13 | key: "ignoreMustache", 14 | type: "checkbox", 15 | label: getMessage("optionsIgnoreMustacheLabel") 16 | }, 17 | { 18 | key: "unicode", 19 | type: "checkbox", 20 | label: getMessage("optionsUnicodeLabel") 21 | }, 22 | { 23 | key: "mail", 24 | type: "checkbox", 25 | label: getMessage("optionsMailLabel") 26 | }, 27 | { 28 | key: "standalone", 29 | type: "checkbox", 30 | label: getMessage("optionsStandaloneLabel"), 31 | children: [ 32 | { 33 | key: "boundaryLeft", 34 | type: "text", 35 | label: getMessage("optionsBoundaryLeftLabel") 36 | }, 37 | { 38 | key: "boundaryRight", 39 | type: "text", 40 | label: getMessage("optionsBoundaryRightLabel") 41 | } 42 | ] 43 | }, 44 | { 45 | key: "customRules", 46 | type: "textarea", 47 | label: getMessage("optionsCustomRulesLabel"), 48 | learnMore: "https://github.com/eight04/linkify-plus-plus?tab=readme-ov-file#custom-rules" 49 | }, 50 | 51 | ] 52 | }, 53 | { 54 | type: "section", 55 | label: getMessage("optionsLinkifierLabel"), 56 | children: [ 57 | { 58 | key: "triggerByPageLoad", 59 | type: "checkbox", 60 | label: getMessage("optionsTriggerByPageLoadLabel") 61 | }, 62 | { 63 | key: "triggerByNewNode", 64 | type: "checkbox", 65 | label: getMessage("optionsTriggerByNewNodeLabel") 66 | }, 67 | { 68 | key: "triggerByHover", 69 | type: "checkbox", 70 | label: getMessage("optionsTriggerByHoverLabel") 71 | }, 72 | { 73 | key: "triggerByClick", 74 | type: "checkbox", 75 | label: getMessage("optionsTriggerByClickLabel") 76 | }, 77 | { 78 | key: "embedImage", 79 | type: "checkbox", 80 | label: getMessage("optionsEmbedImageLabel"), 81 | children: [ 82 | { 83 | key: "embedImageExcludeElement", 84 | type: "textarea", 85 | label: getMessage("optionsEmbedImageExcludeElementLabel"), 86 | validate: validateSelector 87 | } 88 | ] 89 | }, 90 | { 91 | key: "newTab", 92 | type: "checkbox", 93 | label: getMessage("optionsNewTabLabel") 94 | }, 95 | { 96 | key: "excludeElement", 97 | type: "textarea", 98 | label: getMessage("optionsExcludeElementLabel"), 99 | validate: validateSelector 100 | }, 101 | { 102 | key: "includeElement", 103 | type: "textarea", 104 | label: getMessage("optionsIncludeElementLabel"), 105 | validate: validateSelector 106 | }, 107 | { 108 | key: "timeout", 109 | type: "number", 110 | label: getMessage("optionsTimeoutLabel"), 111 | help: getMessage("optionsTimeoutHelp") 112 | }, 113 | { 114 | key: "maxRunTime", 115 | type: "number", 116 | label: getMessage("optionsMaxRunTimeLabel"), 117 | help: getMessage("optionsMaxRunTimeHelp") 118 | }, 119 | ] 120 | }, 121 | ]; 122 | 123 | function validateSelector(value) { 124 | if (value) { 125 | document.documentElement.matches(value); 126 | } 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /src/lib/pref-default.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | fuzzyIp: true, 4 | embedImage: true, 5 | embedImageExcludeElement: ".hljs, .highlight, .brush\\:", 6 | ignoreMustache: false, 7 | unicode: false, 8 | mail: true, 9 | newTab: false, 10 | standalone: false, 11 | boundaryLeft: "{[(\"'", 12 | boundaryRight: "'\")]},.;?!", 13 | excludeElement: ".highlight, .editbox, .brush\\:, .bdsug, .spreadsheetinfo", 14 | includeElement: "", 15 | timeout: 10000, 16 | triggerByPageLoad: false, 17 | triggerByNewNode: false, 18 | triggerByHover: true, 19 | triggerByClick: !supportHover(), 20 | maxRunTime: 100, 21 | customRules: "", 22 | }; 23 | }; 24 | 25 | function supportHover() { 26 | return window.matchMedia("(hover)").matches; 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/triggers/cache.mjs: -------------------------------------------------------------------------------- 1 | export const processedNodes = new WeakSet; 2 | export const nodeValidationCache = new WeakMap; // Node -> boolean 3 | 4 | -------------------------------------------------------------------------------- /src/lib/triggers/click.mjs: -------------------------------------------------------------------------------- 1 | import {linkify} from "linkify-plus-plus-core"; 2 | import {processedNodes} from "./cache.mjs"; 3 | import {validRoot} from "./util.mjs"; 4 | 5 | let options; 6 | 7 | const EVENTS = [ 8 | ["click", handle, {passive: true}], 9 | ] 10 | 11 | function handle(e) { 12 | const el = e.target; 13 | if (validRoot(el, options.validator)) { 14 | processedNodes.add(el); 15 | linkify({...options, root: el, recursive: false}); 16 | } 17 | } 18 | 19 | function enable(_options) { 20 | options = _options; 21 | for (const [event, handler, options] of EVENTS) { 22 | document.addEventListener(event, handler, options); 23 | } 24 | } 25 | 26 | function disable() { 27 | for (const [event, handler, options] of EVENTS) { 28 | document.removeEventListener(event, handler, options); 29 | } 30 | } 31 | 32 | export default { 33 | key: "triggerByClick", 34 | enable, 35 | disable 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/lib/triggers/hover.mjs: -------------------------------------------------------------------------------- 1 | import {linkify} from "linkify-plus-plus-core"; 2 | import {processedNodes} from "./cache.mjs"; 3 | import {validRoot} from "./util.mjs"; 4 | 5 | let options; 6 | 7 | const EVENTS = [ 8 | // catch the first mousemove event since mouseover doesn't fire at page refresh 9 | ["mousemove", handle, {passive: true, once: true}], 10 | ["mouseover", handle, {passive: true}] 11 | ] 12 | 13 | function handle(e) { 14 | const el = e.target; 15 | if (validRoot(el, options.validator)) { 16 | processedNodes.add(el); 17 | linkify({...options, root: el, recursive: false}); 18 | } 19 | } 20 | 21 | function enable(_options) { 22 | options = _options; 23 | for (const [event, handler, options] of EVENTS) { 24 | document.addEventListener(event, handler, options); 25 | } 26 | } 27 | 28 | function disable() { 29 | for (const [event, handler, options] of EVENTS) { 30 | document.removeEventListener(event, handler, options); 31 | } 32 | } 33 | 34 | export default { 35 | key: "triggerByHover", 36 | enable, 37 | disable 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/triggers/index.mjs: -------------------------------------------------------------------------------- 1 | import load from "./load.mjs" 2 | import click from "./click.mjs" 3 | import hover from "./hover.mjs" 4 | import mutation from "./mutation.mjs"; 5 | 6 | export default [load, click, hover, mutation]; 7 | -------------------------------------------------------------------------------- /src/lib/triggers/load.mjs: -------------------------------------------------------------------------------- 1 | import {linkifyRoot, prepareDocument} from "./util.mjs"; 2 | // import {processedNodes} from "./cache.mjs"; 3 | 4 | export default { 5 | key: "triggerByPageLoad", 6 | enable: async options => { 7 | await prepareDocument(); 8 | await linkifyRoot(document.body, options); 9 | }, 10 | disable: () => {} 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/triggers/mutation.mjs: -------------------------------------------------------------------------------- 1 | import {prepareDocument, linkifyRoot} from "./util.mjs"; 2 | import {processedNodes} from "./cache.mjs"; 3 | 4 | const MAX_PROCESSES = 100; 5 | let processes = 0; 6 | let observer; 7 | 8 | async function enable(options) { 9 | await prepareDocument(); 10 | observer = new MutationObserver(function(mutations){ 11 | // Filter out mutations generated by LPP 12 | var lastRecord = mutations[mutations.length - 1], 13 | nodes = lastRecord.addedNodes, 14 | i; 15 | 16 | if (nodes.length >= 2) { 17 | for (i = 0; i < 2; i++) { 18 | if (nodes[i].className == "linkifyplus") { 19 | return; 20 | } 21 | } 22 | } 23 | 24 | for (var record of mutations) { 25 | for (const node of record.addedNodes) { 26 | if (node.nodeType === 1 && !processedNodes.has(node)) { 27 | if (processes >= MAX_PROCESSES) { 28 | throw new Error("Too many processes"); 29 | } 30 | processes++; 31 | linkifyRoot(node, options) 32 | .finally(() => { 33 | processes--; 34 | }); 35 | } 36 | } 37 | } 38 | }); 39 | observer.observe(document.body, { 40 | childList: true, 41 | subtree: true 42 | }); 43 | } 44 | 45 | async function disable() { 46 | await prepareDocument(); 47 | observer && observer.disconnect(); 48 | } 49 | 50 | export default { 51 | key: "triggerByNewNode", 52 | enable, 53 | disable 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/triggers/util.mjs: -------------------------------------------------------------------------------- 1 | import {linkify} from "linkify-plus-plus-core"; 2 | 3 | import { processedNodes, nodeValidationCache } from "./cache.mjs"; 4 | 5 | export async function linkifyRoot(root, options, useIncludeElement = true) { 6 | if (validRoot(root, options.validator)) { 7 | processedNodes.add(root); 8 | await linkify({...options, root, recursive: true}); 9 | } 10 | if (options.includeElement && useIncludeElement) { 11 | for (const el of root.querySelectorAll(options.includeElement)) { 12 | await linkifyRoot(el, options, false); 13 | } 14 | } 15 | } 16 | 17 | export function validRoot(node, validator) { 18 | if (processedNodes.has(node)) { 19 | return false; 20 | } 21 | return getValidation(node); 22 | 23 | function getValidation(p) { 24 | if (!p.parentNode) { 25 | return false; 26 | } 27 | let r = nodeValidationCache.get(p); 28 | if (r === undefined) { 29 | if (validator.isIncluded(p)) { 30 | r = true; 31 | } else if (validator.isExcluded(p)) { 32 | r = false; 33 | } else if (p.parentNode != document.documentElement) { 34 | r = getValidation(p.parentNode); 35 | } else { 36 | r = true; 37 | } 38 | nodeValidationCache.set(p, r); 39 | } 40 | return r; 41 | } 42 | } 43 | 44 | export function prepareDocument() { 45 | // wait till everything is ready 46 | return prepareBody().then(prepareApp); 47 | 48 | function prepareApp() { 49 | const appRoot = document.querySelector("[data-server-rendered]"); 50 | if (!appRoot) { 51 | return; 52 | } 53 | return new Promise(resolve => { 54 | const onChange = () => { 55 | if (!appRoot.hasAttribute("data-server-rendered")) { 56 | resolve(); 57 | observer.disconnect(); 58 | } 59 | }; 60 | const observer = new MutationObserver(onChange); 61 | observer.observe(appRoot, {attributes: true}); 62 | }); 63 | } 64 | 65 | function prepareBody() { 66 | if (document.readyState !== "loading") { 67 | return Promise.resolve(); 68 | } 69 | return new Promise(resolve => { 70 | // https://github.com/Tampermonkey/tampermonkey/issues/485 71 | document.addEventListener("DOMContentLoaded", resolve, {once: true}); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/static/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "dialogOk": { 3 | "message": "OK" 4 | }, 5 | "dialogCancel": { 6 | "message": "Cancel" 7 | }, 8 | "extensionOptions": { 9 | "message": "Linkify Plus Plus Options" 10 | }, 11 | "extensionDescription": { 12 | "message": "Based on Linkify Plus. Turn plain text URLs into links." 13 | }, 14 | "optionsFuzzyIpLabel": { 15 | "message": "Match ambiguous IP addresses." 16 | }, 17 | "optionsIgnoreMustacheLabel": { 18 | "message": "Ignore URLs inside mustaches e.g. {{ ... }}." 19 | }, 20 | "optionsEmbedImageLabel": { 21 | "message": "Create an image element if the URL looks like an image file." 22 | }, 23 | "optionsEmbedImageExcludeElementLabel": { 24 | "message": "Exclude following elements. (CSS selector)" 25 | }, 26 | "optionsUnicodeLabel": { 27 | "message": "Match unicode characters." 28 | }, 29 | "optionsMailLabel": { 30 | "message": "Match email address." 31 | }, 32 | "optionsNewTabLabel": { 33 | "message": "Open links in new tabs." 34 | }, 35 | "optionsStandaloneLabel": { 36 | "message": "The URL must be surrounded by whitespaces." 37 | }, 38 | "optionsLinkifierLabel": { 39 | "message": "Linkifier" 40 | }, 41 | "optionsTriggerByPageLoadLabel": { 42 | "message": "Trigger on page load." 43 | }, 44 | "optionsTriggerByNewNodeLabel": { 45 | "message": "Trigger on dynamically created elements." 46 | }, 47 | "optionsTriggerByHoverLabel": { 48 | "message": "Trigger on mouse over." 49 | }, 50 | "optionsTriggerByClickLabel": { 51 | "message": "Trigger on mouse click." 52 | }, 53 | "optionsBoundaryLeftLabel": { 54 | "message": "Allowed characters between the whitespace and the link. (left side)" 55 | }, 56 | "optionsBoundaryRightLabel": { 57 | "message": "Allowed characters between the whitespace and the link. (right side)" 58 | }, 59 | "optionsExcludeElementLabel": { 60 | "message": "Do not linkify following elements. (CSS selector)" 61 | }, 62 | "optionsIncludeElementLabel": { 63 | "message": "Always linkify following elements. Override above. (CSS selector)" 64 | }, 65 | "optionsTimeoutLabel": { 66 | "message": "Max execution time. (ms)" 67 | }, 68 | "optionsTimeoutHelp": { 69 | "message": "The script will terminate if it takes too long to convert the entire page." 70 | }, 71 | "optionsMaxRunTimeLabel": { 72 | "message": "Max script run time. (ms)" 73 | }, 74 | "optionsMaxRunTimeHelp": { 75 | "message": "If the script takes too long to run, the process would be splitted into small chunks to avoid browser freeze." 76 | }, 77 | "optionsUrlMatcherLabel": { 78 | "message": "URL matcher" 79 | }, 80 | "optionsCustomRulesLabel": { 81 | "message": "Custom rules. (RegExp per line)" 82 | }, 83 | "prefCurrentScopeLabel": { 84 | "message": "Current domain" 85 | }, 86 | "prefAddScopeLabel": { 87 | "message": "Add new domain" 88 | }, 89 | "prefAddScopePrompt": { 90 | "message": "Add new domain" 91 | }, 92 | "prefDeleteScopeLabel": { 93 | "message": "Delete current domain" 94 | }, 95 | "prefDeleteScopeConfirm": { 96 | "message": "Delete domain $DOMAIN$?", 97 | "placeholders": { 98 | "domain": { 99 | "content": "$1", 100 | "example": "www.example.com" 101 | } 102 | } 103 | }, 104 | "prefLearnMoreButton": { 105 | "message": "Learn more" 106 | }, 107 | "prefImportButton": { 108 | "message": "Import" 109 | }, 110 | "prefImportPrompt": { 111 | "message": "Paste settings" 112 | }, 113 | "prefExportButton": { 114 | "message": "Export" 115 | }, 116 | "prefExportPrompt": { 117 | "message": "Copy settings" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/static/_locales/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "dialogOk": { 3 | "message": "Oké" 4 | }, 5 | "dialogCancel": { 6 | "message": "Annuleren" 7 | }, 8 | "extensionOptions": { 9 | "message": "Linkify Plus Plus-instellingen" 10 | }, 11 | "extensionDescription": { 12 | "message": "Gebaseerd op Linkify Plus. Zet platte tekst-url's om in links." 13 | }, 14 | "optionsFuzzyIpLabel": { 15 | "message": "IP-adressen met 4 tekens omzetten" 16 | }, 17 | "optionsIgnoreMustacheLabel": { 18 | "message": "URL's binnen accolades, {{ … }}, negeren" 19 | }, 20 | "optionsEmbedImageLabel": { 21 | "message": "Afbeeldingen insluiten" 22 | }, 23 | "optionsEmbedImageExcludeElementLabel": { 24 | "message": "Elementen uitsluiten (css-selectie):" 25 | }, 26 | "optionsUnicodeLabel": { 27 | "message": "Unicode-tekens omzetten" 28 | }, 29 | "optionsMailLabel": { 30 | "message": "E-mailadressen omzetten" 31 | }, 32 | "optionsNewTabLabel": { 33 | "message": "Links openen op nieuwe tabbladen" 34 | }, 35 | "optionsStandaloneLabel": { 36 | "message": "Alleen links met witruimte rondom omzetten" 37 | }, 38 | "optionsBoundaryLeftLabel": { 39 | "message": "Toegestane tekens tussen witruimtes en links (linkerzijde)" 40 | }, 41 | "optionsBoundaryRightLabel": { 42 | "message": "Toegestane tekens tussen witruimtes en links (rechterzijde)" 43 | }, 44 | "optionsExcludeElementLabel": { 45 | "message": "Elementen uitsluiten (css-selectie):" 46 | }, 47 | "optionsIncludeElementLabel": { 48 | "message": "Elementen die bovenstaande negeren (css-selectie):" 49 | }, 50 | "optionsTimeoutLabel": { 51 | "message": "Max. uitvoertijd (in ms):" 52 | }, 53 | "optionsTimeoutHelp": { 54 | "message": "Het script wordt onderbroken als het te lang duurt om alle links op de gehele pagina om te zetten." 55 | }, 56 | "optionsMaxRunTimeLabel": { 57 | "message": "Max. scriptuitvoertijd (in ms)" 58 | }, 59 | "optionsMaxRunTimeHelp": { 60 | "message": "Verdeel het proces in kleinere stukken om te voorkomen dat de browser vastloopt." 61 | }, 62 | "optionsCustomRulesLabel": { 63 | "message": "Eigen regels (één reguliere uitdrukking per regel):" 64 | }, 65 | "prefCurrentScopeLabel": { 66 | "message": "Huidig domein" 67 | }, 68 | "prefAddScopeLabel": { 69 | "message": "Domein toevoegen" 70 | }, 71 | "prefAddScopePrompt": { 72 | "message": "Domein toevoegen" 73 | }, 74 | "prefDeleteScopeLabel": { 75 | "message": "Huidig domein verwijderen" 76 | }, 77 | "prefDeleteScopeConfirm": { 78 | "message": "Weet je zeker dat je $DOMAIN$ wilt verwijderen?", 79 | "placeholders": { 80 | "domain": { 81 | "content": "$1", 82 | "example": "www.voorbeeld.nl" 83 | } 84 | } 85 | }, 86 | "prefLearnMoreButton": { 87 | "message": "Meer informatie" 88 | }, 89 | "prefImportButton": { 90 | "message": "Importeren" 91 | }, 92 | "prefImportPrompt": { 93 | "message": "Instellingen plakken" 94 | }, 95 | "prefExportButton": { 96 | "message": "Exporteren" 97 | }, 98 | "prefExportPrompt": { 99 | "message": "Instellingen kopiëren" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/static/css/dialog.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 16px; 3 | } 4 | textarea, button { 5 | font-size: inherit; 6 | } 7 | -------------------------------------------------------------------------------- /src/static/css/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 6px; 3 | } 4 | 5 | button.browser-style, 6 | select.browser-style { 7 | font-size: inherit; 8 | height: auto; 9 | padding: 4px 8px; 10 | } 11 | 12 | /* root */ 13 | .pref-root { 14 | font-size: 15px; 15 | line-height: 1.5; 16 | } 17 | 18 | /* toolbar */ 19 | .webext-pref-toolbar { 20 | display: flex; 21 | justify-content: center; 22 | margin-bottom: 16px; 23 | } 24 | .webext-pref-toolbar button { 25 | min-width: 8em; 26 | text-align: center; 27 | } 28 | .webext-pref-toolbar button:not(:first-child) { 29 | margin-left: 8px; 30 | } 31 | 32 | /* nav */ 33 | .webext-pref-nav { 34 | display: flex; 35 | margin-bottom: 12px; 36 | } 37 | .webext-pref-nav select { 38 | flex-grow: 1; 39 | } 40 | .webext-pref-nav select, 41 | .webext-pref-nav button { 42 | min-width: 30px; 43 | text-align: center; 44 | text-align-last: center; 45 | } 46 | .webext-pref-nav button { 47 | margin-left: 8px; 48 | } 49 | 50 | /* checkbox */ 51 | .webext-pref-checkbox { 52 | margin: 8px 0; 53 | padding-left: 24px; 54 | } 55 | .webext-pref-checkbox::before { 56 | content: ""; 57 | margin-left: -24px; 58 | } 59 | .webext-pref-checkbox > input { 60 | font: inherit; 61 | width: 1em; 62 | height: 1em; 63 | margin: 0 calc(24px - 1em) 0 0; 64 | vertical-align: middle; 65 | } 66 | .webext-pref-checkbox > label { 67 | vertical-align: middle; 68 | } 69 | .webext-pref-checkbox-children { 70 | margin: 6px 0; 71 | padding: 0; 72 | border-width: 0; 73 | } 74 | .webext-pref-checkbox-children[disabled] { 75 | opacity: 0.5; 76 | } 77 | .webext-pref-checkbox-children > :first-child { 78 | margin-top: 0; 79 | } 80 | .webext-pref-checkbox-children > :last-child { 81 | margin-bottom: 0; 82 | } 83 | .webext-pref-checkbox-children > :last-child > :last-child { 84 | margin-bottom: 0; 85 | } 86 | 87 | /* text */ 88 | .webext-pref-text, 89 | .webext-pref-number, 90 | .webext-pref-textarea { 91 | margin: 8px 0; 92 | } 93 | .webext-pref-text input, 94 | .webext-pref-number input, 95 | .webext-pref-textarea textarea { 96 | padding: 3px 6px; 97 | display: block; 98 | width: 100%; 99 | font: inherit; 100 | box-sizing: border-box; 101 | } 102 | .webext-pref-textarea textarea { 103 | height: 6em; 104 | } 105 | 106 | /* help */ 107 | .webext-pref-help { 108 | margin: 4px 0 6px; 109 | color: #737373; 110 | } 111 | -------------------------------------------------------------------------------- /src/static/dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/static/icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/static/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Linkify Plus Plus", 3 | "version": "12.0.1", 4 | "description": "__MSG_extensionDescription__", 5 | "homepage_url": "https://github.com/eight04/linkify-plus-plus", 6 | "developer": { 7 | "name": "eight04", 8 | "url": "https://github.com/eight04" 9 | }, 10 | "content_scripts": [ 11 | { 12 | "matches": [""], 13 | "js": [], 14 | "run_at": "document_start" 15 | } 16 | ], 17 | "default_locale": "en", 18 | "manifest_version": 2, 19 | "options_ui": { 20 | "page": "options.html" 21 | }, 22 | "browser_action": { 23 | "default_icon": "icon.svg", 24 | "default_title": "__MSG_extensionOptions__", 25 | "theme_icons": [ 26 | { 27 | "dark": "icon.svg", 28 | "light": "icon-light.svg", 29 | "size": 32 30 | } 31 | ] 32 | }, 33 | "background": { 34 | "scripts": [] 35 | }, 36 | "permissions": [ 37 | "storage", 38 | "activeTab" 39 | ], 40 | "browser_specific_settings": { 41 | "gecko": { 42 | }, 43 | "gecko_android": { 44 | "strict_min_version": "113.0" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/static/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /src/userscript/index.js: -------------------------------------------------------------------------------- 1 | const translate = require("../static/_locales/en/messages.json"); // default 2 | const GM_webextPref = require("gm-webext-pref"); 3 | const prefDefault = require("../lib/pref-default"); 4 | const prefBody = require("../lib/pref-body"); 5 | const {startLinkifyPlusPlus} = require("../lib/main"); 6 | 7 | function getMessageFactory() { 8 | return (key, params) => { 9 | if (!params) { 10 | return translate[key]; 11 | } 12 | if (!Array.isArray(params)) { 13 | params = [params]; 14 | } 15 | return translate[key].replace(/\$\d/g, m => { 16 | const index = Number(m.slice(1)); 17 | return params[index - 1]; 18 | }); 19 | }; 20 | } 21 | 22 | startLinkifyPlusPlus(async () => { 23 | const getMessage = getMessageFactory(); 24 | const pref = GM_webextPref({ 25 | default: prefDefault(), 26 | body: prefBody(getMessage), 27 | getMessage, 28 | getNewScope: () => location.hostname 29 | }); 30 | await pref.ready(); 31 | await pref.setCurrentScope(location.hostname); 32 | return pref; 33 | }); 34 | --------------------------------------------------------------------------------