您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically turns plain-text URLs in Digg posts and comments into clickable hyperlinks
// ==UserScript== // @name Digg Auto-Linker // @namespace Z4CK-tools // @version 1.3 // @description Automatically turns plain-text URLs in Digg posts and comments into clickable hyperlinks // @author Z4CK // @match https://*.digg.com/* // @run-at document-start // @grant none // @license GPL-3.0-only // ==/UserScript== (() => { 'use strict'; const SKIP_SELECTOR = 'a, code, pre, script, style, textarea, head, title, noscript'; const SKIP_TAGS = new Set(['A', 'CODE', 'PRE', 'SCRIPT', 'STYLE', 'TEXTAREA', 'HEAD', 'TITLE', 'NOSCRIPT']); const URL_PATTERN = /\b((?:https?:\/\/|www\.)[^\s<>"'()]+)(?=$|[\s<>\p{P}])/gu; const TRAILING_PUNCTUATION = /[)\]}.,!?:;'"]+$/; // Batch to avoid dupes const queue = new Set(); let flushing = false; function schedule(node) { if (!node) return; queue.add(node); if (!flushing) { flushing = true; requestAnimationFrame(flushQueue); } } function flushQueue() { for (const node of queue) { processNode(node); } queue.clear(); flushing = false; } function processNode(node) { if (!node) return; if (node.nodeType === Node.TEXT_NODE) { const parent = node.parentElement; if (!parent) return; if (parent.closest(SKIP_SELECTOR)) return; linkifyTextNode(node); return; } if (node.nodeType === Node.ELEMENT_NODE) { if (SKIP_TAGS.has(node.tagName)) return; for (const child of node.childNodes) { processNode(child); } } } function linkifyTextNode(textNode) { const text = textNode.textContent; URL_PATTERN.lastIndex = 0; let match; let lastIndex = 0; let replaced = false; const fragment = document.createDocumentFragment(); while ((match = URL_PATTERN.exec(text)) !== null) { const start = match.index; const end = start + match[0].length; if (start > lastIndex) { fragment.appendChild(document.createTextNode(text.slice(lastIndex, start))); } let displayText = match[1]; let trailing = ''; const trailingMatch = displayText.match(TRAILING_PUNCTUATION); if (trailingMatch) { trailing = trailingMatch[0]; displayText = displayText.slice(0, -trailing.length); } if (!displayText) { fragment.appendChild(document.createTextNode(match[0])); lastIndex = end; continue; } const link = document.createElement('a'); link.href = displayText.startsWith('www.') ? `https://${displayText}` : displayText; link.textContent = displayText; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.style.color = 'inherit'; link.style.textDecoration = 'underline'; fragment.appendChild(link); if (trailing) fragment.appendChild(document.createTextNode(trailing)); lastIndex = end; replaced = true; } if (!replaced) return; if (lastIndex < text.length) { fragment.appendChild(document.createTextNode(text.slice(lastIndex))); } textNode.parentNode?.replaceChild(fragment, textNode); } // Track live edits const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'characterData') { schedule(mutation.target); } for (const added of mutation.addedNodes) { if (added.nodeType === Node.ELEMENT_NODE || added.nodeType === Node.TEXT_NODE) { schedule(added); } } } }); observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true }); schedule(document.documentElement); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => schedule(document.body), { once: true }); } else { schedule(document.body); } window.addEventListener('beforeunload', () => observer.disconnect(), { once: true }); })();