OTTOhub Linker

为OTTOhub的某些纯文本代号添加超链接,支持移动端和桌面端

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         OTTOhub Linker
// @namespace    https://www.ottohub.cn/space/11481
// @version      1.3.2
// @description  为OTTOhub的某些纯文本代号添加超链接,支持移动端和桌面端
// @author       Gemini&OctoberSama
// @license      WTFPL 2.0
// @match        https://www.ottohub.cn/*
// @match        https://m.ottohub.cn/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    /**
     * 1. 正则表达式配置
     * (ov|ob|av|sm)(\d+) : 前缀+纯数字
     * (bv)([a-zA-Z0-9]+) : bv+字母数字
     * (uid)([:: ]?)(\d+) : uid+分隔符+数字
     */
    const regex = /(ov|ob|av|sm)(\d+)|(bv)([a-zA-Z0-9]+)|(uid)([:: ]?)(\d+)/gi;

    /**
     * 2. 禁止替换的标签选择器列表 (黑名单)
     */
    const forbiddenSelector = [
        'a', 'script', 'style', 'textarea', 'input', 'button', 'select', 'option', 'optgroup', 'label',
        'code', 'pre', '[contenteditable="true"]',
        'video', 'audio', 'img', 'svg', 'canvas', 'map', 'area', 'track',
        'embed', 'object', 'iframe', 'param', 'source'
    ].join(', ');

    /**
     * 生成对应的 URL
     */
    function generateUrl(prefix, content) {
        const p = prefix.toLowerCase();
        const hostname = window.location.hostname;
        const isMobile = hostname === 'm.ottohub.cn';

        if (p === 'sm') return `https://www.nicovideo.jp/watch/${prefix}${content}`;
        if (p === 'av' || p === 'bv') return `https://www.bilibili.com/video/${prefix}${content}`;
        if (p === 'ov') return `/v/${content}`;
        if (p === 'ob') return isMobile ? `/b/${content}` : `/blog/detail/${content}`;
        if (p === 'uid') return isMobile ? `/u/${content}` : `/space/${content}`;
        return '#';
    }

    /**
     * 【核心逻辑】处理单个文本节点
     * 将原来嵌套在 TreeWalker 里的逻辑提取出来
     */
    function handleTextNode(node) {
        // 1. 基本检查
        if (!node.nodeValue) return;

        // 2. 黑名单检查 (检查父级)
        if (node.parentElement && node.parentElement.closest(forbiddenSelector)) {
            return;
        }

        // 3. 正则预检测 (避免不必要的计算)
        regex.lastIndex = 0;
        if (!regex.test(node.nodeValue)) {
            return;
        }

        // 4. 执行替换
        const text = node.nodeValue;
        const fragment = document.createDocumentFragment();
        let lastIndex = 0;
        let match;

        regex.lastIndex = 0; // 确保从头开始

        while ((match = regex.exec(text)) !== null) {
            // 添加匹配前的文本
            const plainText = text.substring(lastIndex, match.index);
            if (plainText) {
                fragment.appendChild(document.createTextNode(plainText));
            }

            // 解析匹配项
            let prefix, content;
            if (match[1]) {
                prefix = match[1]; content = match[2];
            } else if (match[3]) {
                prefix = match[3]; content = match[4];
            } else {
                prefix = match[5]; content = match[7];
            }

            // 创建链接
            const link = document.createElement('a');
            link.href = generateUrl(prefix, content);
            link.textContent = match[0];
            link.target = "_blank";
            link.className = "ottohub-auto-link";
            // 移动端可能需要强制一点样式来显示链接感,或者继承默认
            // link.style.color = "#039be5";

            fragment.appendChild(link);

            lastIndex = regex.lastIndex;
        }

        // 添加剩余文本
        const remainingText = text.substring(lastIndex);
        if (remainingText) {
            fragment.appendChild(document.createTextNode(remainingText));
        }

        // 只有当真正发生了替换时才修改 DOM
        if (lastIndex > 0) {
            node.parentNode.replaceChild(fragment, node);
        }
    }

    /**
     * 遍历节点逻辑
     */
    function processNode(root) {
        // 【关键修复】如果传入的直接是文本节点,直接处理,不再走 TreeWalker
        if (root.nodeType === Node.TEXT_NODE) {
            handleTextNode(root);
            return;
        }

        // 如果是元素节点,才使用 TreeWalker 遍历其内部
        const walker = document.createTreeWalker(
            root,
            NodeFilter.SHOW_TEXT,
            null, // 过滤逻辑已移至 handleTextNode 内部
            false
        );

        const nodesToProcess = [];
        while (walker.nextNode()) {
            nodesToProcess.push(walker.currentNode);
        }

        nodesToProcess.forEach(handleTextNode);
    }

    // 1. 初始执行
    processNode(document.body);

    // 2. 监听动态加载
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            // 情况A:新增了节点 (包含 TextNode 或 Element)
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    // 忽略脚本自身添加的链接
                    if (node.nodeType === 1 && node.classList.contains("ottohub-auto-link")) return;
                    processNode(node);
                });
            }
            // 情况B:文本节点的内容直接改变了 (Vue等框架常见操作)
            else if (mutation.type === 'characterData') {
                // 直接处理变动的文本节点
                handleTextNode(mutation.target);
            }
        });
    });

    // 配置监听:增加 characterData 以捕捉纯文本变化
    observer.observe(document.body, {
        childList: true,
        subtree: true,
        characterData: true
    });

})();