Linkify Plus Plus

Based on Linkify Plus. Turn plain text URLs into links.

目前为 2018-05-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Linkify Plus Plus
  3. // @version 8.2.1
  4. // @description Based on Linkify Plus. Turn plain text URLs into links.
  5. // @license BSD-3-Clause
  6. // @homepageURL https://github.com/eight04/linkify-plus-plus
  7. // @supportURL https://github.com/eight04/linkify-plus-plus/issues
  8. // @namespace eight04.blogspot.com
  9. // @include *
  10. // @exclude https://www.google.*/search*
  11. // @exclude https://www.google.*/webhp*
  12. // @exclude https://music.google.com/*
  13. // @exclude https://mail.google.com/*
  14. // @exclude https://docs.google.com/*
  15. // @exclude https://encrypted.google.com/*
  16. // @exclude http://mxr.mozilla.org/*
  17. // @exclude http://w3c*.github.io/*
  18. // @require https://gf.qytechs.cn/scripts/7212-gm-config-eight-s-version/code/GM_config%20(eight's%20version).js?version=156587
  19. // @require https://gf.qytechs.cn/scripts/27630-linkify-plus-plus-core/code/linkify-plus-plus-core.js?version=213494
  20. // @grant GM_addStyle
  21. // @grant GM_registerMenuCommand
  22. // @grant GM_getValue
  23. // @grant GM_setValue
  24. // @grant unsafeWindow
  25. // @compatible firefox Tampermonkey latest
  26. // @compatible chrome Tampermonkey latest
  27. // ==/UserScript==
  28.  
  29. /* globals GM_config */
  30.  
  31. (function(){
  32.  
  33. // Limit contentType to "text/plain" or "text/html"
  34. if (document.contentType != undefined && document.contentType != "text/plain" && document.contentType != "text/html") {
  35. return;
  36. }
  37.  
  38. var {linkify, UrlMatcher, INVALID_TAGS} = window.linkifyPlusPlusCore;
  39.  
  40. const BUFFER_SIZE = 100;
  41.  
  42. // Valid root node before linkifing
  43. function validRoot(node, validator) {
  44. // Cache valid state in node.VALID
  45. if (node.VALID !== undefined) {
  46. return node.VALID;
  47. }
  48.  
  49. // Loop through ancestor
  50. var cache = [], isValid;
  51. while (node != document.documentElement) {
  52. cache.push(node);
  53.  
  54. // It is invalid if it has invalid ancestor
  55. if (!validator(node) || INVALID_TAGS[node.nodeName]) {
  56. isValid = false;
  57. break;
  58. }
  59.  
  60. // The node was removed from DOM tree
  61. if (!node.parentNode) {
  62. return false;
  63. }
  64.  
  65. node = node.parentNode;
  66.  
  67. if (node.VALID !== undefined) {
  68. isValid = node.VALID;
  69. break;
  70. }
  71. }
  72.  
  73. // All ancestors are fine
  74. if (isValid === undefined) {
  75. isValid = true;
  76. }
  77.  
  78. // Cache the result
  79. var i;
  80. for (i = 0; i < cache.length; i++) {
  81. cache[i].VALID = isValid;
  82. }
  83.  
  84. return isValid;
  85. }
  86.  
  87. function createValidator({selector, skipSelector}) {
  88. return function(node) {
  89. if (node.isContentEditable) {
  90. return false;
  91. }
  92. if (selector && node.matches && node.matches(selector)) {
  93. return true;
  94. }
  95. if (skipSelector && node.matches && node.matches(skipSelector)) {
  96. return false;
  97. }
  98. return true;
  99. };
  100. }
  101.  
  102. function selectorTest(selector) {
  103. try {
  104. document.documentElement.matches(selector);
  105. } catch (err) {
  106. alert(`Invalid selector: ${selector}`);
  107. return false;
  108. }
  109. return true;
  110. }
  111.  
  112. function createList(text) {
  113. text = text.trim();
  114. if (!text) {
  115. return null;
  116. }
  117. return text.split("\n");
  118. }
  119.  
  120. function createBuffer(size) {
  121. const set = new Set;
  122. const buff = Array(size);
  123. const eventBus = document.createElement("span");
  124. let start = 0;
  125. let end = 0;
  126. return {push, eventBus, shift};
  127. function push(item) {
  128. if (set.has(item)) {
  129. return;
  130. }
  131. if (set.size && start === end) {
  132. // overflow
  133. eventBus.dispatchEvent(new CustomEvent("overflow"));
  134. set.clear();
  135. return;
  136. }
  137. set.add(item);
  138. buff[end] = item;
  139. end = (end + 1) % size;
  140. eventBus.dispatchEvent(new CustomEvent("add"));
  141. }
  142. function shift() {
  143. if (!set.size) {
  144. return;
  145. }
  146. const item = buff[start];
  147. set.delete(item);
  148. buff[start] = null;
  149. start = (start + 1) % size;
  150. return item;
  151. }
  152. }
  153.  
  154. function createLinkifyProcess(options) {
  155. const buffer = createBuffer(BUFFER_SIZE);
  156. let overflowed = false;
  157. let started = false;
  158. buffer.eventBus.addEventListener("add", start);
  159. buffer.eventBus.addEventListener("overflow", () => overflowed = true);
  160. return {process};
  161. function process(root) {
  162. if (overflowed) {
  163. return false
  164. }
  165. if (validRoot(root, options.validator)) {
  166. buffer.push(root);
  167. }
  168. return true;
  169. }
  170. function start() {
  171. if (started) {
  172. return;
  173. }
  174. started = true;
  175. deque();
  176. }
  177. function deque() {
  178. let root;
  179. if (overflowed) {
  180. root = document.body;
  181. overflowed = false;
  182. } else {
  183. root = buffer.shift();
  184. }
  185. if (!root) {
  186. started = false;
  187. return;
  188. }
  189. linkify(root, options)
  190. .then(() => {
  191. var p = Promise.resolve();
  192. if (options.selector) {
  193. for (var node of root.querySelectorAll(options.selector)) {
  194. p = p.then(linkify.bind(null, node, options));
  195. }
  196. }
  197. return p;
  198. })
  199. .catch(err => {
  200. console.error(err);
  201. })
  202. .then(deque);
  203. }
  204. }
  205.  
  206. function createOptions() {
  207. const options = {};
  208. setup();
  209. return options;
  210. function setup() {
  211. GM_config.setup({
  212. ip: {
  213. label: "Match 4 digits IP",
  214. type: "checkbox",
  215. default: true
  216. },
  217. image: {
  218. label: "Embed images",
  219. type: "checkbox",
  220. default: true
  221. },
  222. imageSkipSelector: {
  223. label: "Don't embed images under following elements",
  224. type: "textarea",
  225. default: ".hljs, .highlight, .brush\\:"
  226. },
  227. unicode: {
  228. label: "Allow non-ascii character",
  229. type: "checkbox",
  230. default: false
  231. },
  232. newTab: {
  233. label: "Open link in new tab",
  234. type: "checkbox",
  235. default: false
  236. },
  237. standalone: {
  238. label: "URL must be surrounded by whitespace",
  239. type: "checkbox",
  240. default: false
  241. },
  242. boundaryLeft: {
  243. label: "Boundary characters between whitespace and URL (left)",
  244. type: "text",
  245. default: "{[(\"'"
  246. },
  247. boundaryRight: {
  248. label: "Boundary characters between whitespace and URL (right)",
  249. type: "text",
  250. default: "'\")]},.;?!"
  251. },
  252. skipSelector: {
  253. label: "Do not linkify these elements. (CSS selector)",
  254. type: "textarea",
  255. default: ".highlight, .editbox, .brush\\:, .bdsug, .spreadsheetinfo"
  256. },
  257. selector: {
  258. label: "Always linkify these elements, override above. (CSS selector)",
  259. type: "textarea",
  260. default: ""
  261. },
  262. timeout: {
  263. label: "Max execution time (ms).",
  264. type: "number",
  265. default: 10000
  266. },
  267. maxRunTime: {
  268. label: "Max script run time (ms). If the script is freezing your browser, try to decrease this value.",
  269. type: "number",
  270. default: 100
  271. },
  272. customRules: {
  273. label: "Custom rules. One pattern per line. (RegExp)",
  274. type: "textarea",
  275. default: ""
  276. }
  277. }, function() {
  278. Object.assign(options, GM_config.get());
  279. if (options.selector && !selectorTest(options.selector)) {
  280. options.selector = null;
  281. }
  282. if (options.skipSelector && !selectorTest(options.skipSelector)) {
  283. options.skipSelector = null;
  284. }
  285. if (options.customRules) {
  286. options.customRules = createList(options.customRules);
  287. }
  288. options.validator = createValidator(options);
  289. options.fuzzyIp = options.ip;
  290. options.ignoreMustache = unsafeWindow.angular || unsafeWindow.Vue;
  291. options.embedImage = options.image;
  292. options.matcher = new UrlMatcher(options);
  293. options.onlink = options.imageSkipSelector ? onlink : null;
  294. });
  295. }
  296. function onlink({link, range, content}) {
  297. if (link.childNodes[0].nodeName != "IMG") return;
  298. var parent = range.startContainer;
  299. // it might be a text node
  300. if (!parent.closest) {
  301. parent = parent.parentNode;
  302. }
  303. if (!parent.closest(options.imageSkipSelector)) return;
  304. // remove image
  305. link.innerHTML = "";
  306. link.appendChild(content);
  307. }
  308. }
  309.  
  310. // Program init
  311. GM_addStyle(".linkifyplus img { max-width: 90%; }");
  312. const linkifyProcess = createLinkifyProcess(createOptions());
  313. const observer = new MutationObserver(function(mutations){
  314. // Filter out mutations generated by LPP
  315. var lastRecord = mutations[mutations.length - 1],
  316. nodes = lastRecord.addedNodes,
  317. i;
  318.  
  319. if (nodes.length >= 2) {
  320. for (i = 0; i < 2; i++) {
  321. if (nodes[i].className == "linkifyplus") {
  322. return;
  323. }
  324. }
  325. }
  326.  
  327. for (var record of mutations) {
  328. if (record.addedNodes.length) {
  329. if (!linkifyProcess.process(record.target)) {
  330. // it's full
  331. break;
  332. }
  333. }
  334. }
  335. });
  336.  
  337. function init() {
  338. observer.observe(document.body, {
  339. childList: true,
  340. subtree: true
  341. });
  342. linkifyProcess.process(document.body);
  343. }
  344.  
  345. if (document.body) {
  346. init();
  347. } else {
  348. // https://github.com/Tampermonkey/tampermonkey/issues/485
  349. document.addEventListener("DOMContentLoaded", init, {once: true});
  350. }
  351.  
  352. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址