OTTOhub Linker

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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
    });

})();