Greasy Fork

Greasy Fork is available in English.

网页文本转链接

高性能文本转链接方案,支持动态内容

// ==UserScript==
// @name        网页文本转链接
// @description   高性能文本转链接方案,支持动态内容
// @version      1.0
// @author       WJ
// @match       *://*/*
// @exclude      https://*.bing.com/*
// @exclude      https://*.baidu.com/*
// @license       MIT
// @grant        none
// @run-at       document-idle
// @namespace   http://greasyfork.icu/users/914996
// ==/UserScript==

(() => {
  // 1. 注入样式
  document.head.insertAdjacentHTML(
    'beforeend',
    '<style>.url-link{color:#348A87;text-decoration:underline}</style>'
  );

  // 2. URL 正则与常量
  const tlds = [
    'app','aero','aer','art','asia','beer','biz','cat','cc','chat','ci','cloud',
    'club','cn','com','cool','coop','co','dev','edu','email','fit','fun','gov',
    'group','hk','host','icu','info','ink','int','io','jobs','kim','love','ltd',
    'luxe','me','mil','mobi','moe','museum','name','net','nl','network','one',
    'online','org','plus','post','press','pro','red','ren','run','ru','shop',
    'site','si','space','store','tech','tel','top','travel','tv','tw','uk','us',
    'video','vip','wang','website','wiki','wml','work','ws','xin','xyz','yoga','zone'
  ].join('|');
  const urlRegex = new RegExp(
    String.raw`\b[\w.:/?=%&#-]{3,}\.(?:${tlds})(?!\w)[\w.:/?=%&#-]*|` +
    String.raw`(?:(?:https?:\/\/)|(?:www\.|wap\.))[\w.:/?=%&#-@+~=]{3,250}\.[\w]{2,6}\b[\w.:/?=%&#-@+~=]*`,
    'gi'
  );

  // 3. 工具函数 - 优化跳过检测逻辑
  const skipTags = new Set(['A','SCRIPT','STYLE','TEXTAREA','BUTTON','SELECT','OPTION','CODE','PRE','INPUT']);
  const skipSelectors = [
    '[contenteditable]', 'code', 'pre', '.WJ_modal',
    '.modal', '.popup', '.dialog', '[role="dialog"]',
    '.ace_editor', '.CodeMirror', '.monaco-editor', '.cm-editor'
  ].join(',');
  const PROCESSED = 'data-url-processed';
  const shouldSkip = el => !el || skipTags.has(el.tagName) || el.isContentEditable || !!el.closest?.(skipSelectors);

  // 4. 节点处理函数
  const processNode = root => {
    if (!root || root.hasAttribute?.(PROCESSED) || shouldSkip(root)) return;
    root.setAttribute(PROCESSED, 'true');
    const walker = document.createTreeWalker(
      root,
      NodeFilter.SHOW_TEXT,
      { acceptNode: n => shouldSkip(n.parentElement) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT }
    );
    const tasks = [];
    for (let node; (node = walker.nextNode());) {
      const o = node.textContent ?? '';
      const r = o.replace(urlRegex, m => {
        const h = /^\w+:\/\//.test(m) ? m : `https://${m}`;
        return `<a class="url-link" target="_blank" rel="noopener noreferrer" href="${h}">${m}</a>`;
      });
      r !== o && tasks.push({ node, replaced: r });
    }
    for (const { node, replaced } of tasks) {
      node.replaceWith(document.createRange().createContextualFragment(replaced));
    }
  };

  // 5. 观察器
  const io = new IntersectionObserver(entries => {
    for (const { isIntersecting, target } of entries) {
      if (isIntersecting) {
        io.unobserve(target);
        requestIdleCallback?.(() => processNode(target), { timeout: 1000 });
      }
    }
  });
  const mo = new MutationObserver(mutations => {
    for (const { addedNodes } of mutations) {
      for (const node of addedNodes) {
        if (node.nodeType === 1 && !shouldSkip(node) && !node.hasAttribute?.(PROCESSED)) {
          io.observe(node);
        }
      }
    }
  });

  // 7. 初始化
  Array.from(document.body.children).forEach(child => !shouldSkip(child) && io.observe(child));
  mo.observe(document.body, { childList: true, subtree: true });
})();