网页文本转链接

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

// ==UserScript==
// @name        网页文本转链接
// @description   高性能文本转链接方案,支持动态内容
// @version      1.0
// @author       WJ
// @match       *://*/*
// @license       MIT
// @grant        none
// @run-at       document-idle
// @namespace   https://greasyfork.org/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(?![^\s<>"']*(?:https?:))[\w.:/?=%&#-]{3,}\.(?:${tlds})(?!\w)[\w.:/?=%&#-]*|` +
    String.raw`(?:(?:https?:\/\/)|(?:www\.|wap\.))[\w.:/?=%&#-@+~]{3,250}\.[\w]{2,6}\b[\w.:/?=%&#-@+~]*`,
    'gi'
  );

  // 3. 常量
  const skipTags = `:is(a,applet,area,button,canvas,code,cite,embed,frame,frameset,head,
    iframe,img,input,map,meta,noscript,object,option,pre,script,select,style,svg,textarea)`.replace(/\s+/g, '');
    
  const skipSelectors = `[contenteditable],.WJ_modal,.ace_editor,.CodeMirror,.monaco-editor,.cm-editor`.replace(/\s+/g, '');

  // 4. 仅负责检查
  const BJ = 'data-url-bj';
  const guard = root =>
    !( !root ||
      root.hasAttribute?.(BJ) ||
      root.closest?.(skipTags) ||
      root.closest?.(skipSelectors)
    );

  // 5. 观察器
  const mo = new MutationObserver(mutations => {
    for (const { addedNodes } of mutations) {
      for (const node of addedNodes) {
        if (node.nodeType === 1 && guard(node)) io.observe(node);
      }
    }
  });
  const io = new IntersectionObserver(entries => {
    for (const { isIntersecting, target } of entries) {
      if (isIntersecting) {
        io.unobserve(target);
        requestIdleCallback?.(() => guardok(target), { timeout: 1000 });
      }
    }
  });
  
  // 6. 处理节点
  const guardok = root => {
    const walker = document.createTreeWalker(root, 4, {
      acceptNode: n => guard(n.parentElement) ? 1 : 2
    });
    const tasks = [];
    for (let node; (node = walker.nextNode());) {
      const raw = node.textContent ?? '';
      const replaced = raw.replace(urlRegex, m => 
        `<a class="url-link" target="_self" href="${/^\w+:\/\//.test(m) ? m : 'https://' + m}">${m}</a>`
      );
      raw !== replaced && tasks.push({ node, replaced });
    }
    tasks.forEach(({ node, replaced }) => 
      node.replaceWith(document.createRange().createContextualFragment(replaced))
    );
    root.setAttribute(BJ, 'true');
  };

  // 7. 初始化
  setTimeout(() => {
    [...document.body.children].forEach(el => guard(el) && io.observe(el));
    mo.observe(document.body, { childList: true, subtree: true });
  }, 1000);
})();