【v7.6 · 回归初心最终版】全局字体与RTL自适应

【回归本源】放弃复杂的祖先查找,回归简单直接的元素处理,确保LI和它的父容器OL/UL都被正确设置RTL。

// ==UserScript==
// @name         【v7.6 · 回归初心最终版】全局字体与RTL自适应
// @namespace    http://tampermonkey.net/
// @version     7.6
// @license MIT
// @description  【回归本源】放弃复杂的祖先查找,回归简单直接的元素处理,确保LI和它的父容器OL/UL都被正确设置RTL。
// @author       YourName & AI Assistant
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置部分 ---
    const DEFAULTS = {
        fontName: 'ukij ekran',
        enableFontReplace: true,
        enableRtl: true,
        excludedSites: '',
        debounceTime: 150
    };
    const config = {
        fontName: GM_getValue('fontName', DEFAULTS.fontName),
        enableFontReplace: GM_getValue('enableFontReplace', DEFAULTS.enableFontReplace),
        enableRtl: GM_getValue('enableRtl', DEFAULTS.enableRtl),
        excludedSites: GM_getValue('excludedSites', DEFAULTS.excludedSites).split(',').map(s => s.trim()).filter(Boolean),
        debounceTime: GM_getValue('debounceTime', DEFAULTS.debounceTime)
    };
    if (config.excludedSites.some(site => window.location.hostname.includes(site))) return;

    const newFont = `"${config.fontName}"`;
    const rtlLangRegex = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;

    // --- 样式注入 (Shadow DOM 穿透) ---
    const styleContent = `:host, * { font-family: ${newFont}, sans-serif !important; }`;
    function applyStyles(root) {
        if (!config.enableFontReplace || root.querySelector('.gm-font-style')) return;
        try {
            const style = document.createElement('style');
            style.className = 'gm-font-style';
            style.textContent = styleContent;
            root.appendChild(style);
        } catch (e) {}
    }
    const originalAttachShadow = Element.prototype.attachShadow;
    Element.prototype.attachShadow = function (options) {
        const shadowRoot = originalAttachShadow.call(this, options);
        applyStyles(shadowRoot);
        return shadowRoot;
    };

    // --- 【RTL 核心逻辑 · 回归初心版】 ---
    function runFullScan(scopeNode) {
        if (!config.enableRtl || !scopeNode) return;

        const elementsToProcess = new Set();
        const walker = document.createTreeWalker(scopeNode, NodeFilter.SHOW_TEXT, {
            acceptNode: node => rtlLangRegex.test(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
        });

        while(walker.nextNode()) {
            let el = walker.currentNode.parentElement;
            if (el && !el.hasAttribute('dir')) {
                // 1. 添加文本的直接父元素
                elementsToProcess.add(el);

                // 2. 如果是列表项,也添加其父列表容器
                if (el.tagName === 'LI') {
                    const listParent = el.parentElement;
                    if (listParent && (listParent.tagName === 'UL' || listParent.tagName === 'OL')) {
                        elementsToProcess.add(listParent);
                    }
                }
            }
        }

        // 3. 一次性处理所有收集到的元素
        elementsToProcess.forEach(el => {
            el.dir = 'rtl';
        });

        // 递归处理 Shadow DOM
        scopeNode.querySelectorAll('*').forEach(el => {
            if (el.shadowRoot) runFullScan(el.shadowRoot);
        });
    }

    // --- 【输入框智能切换逻辑】 ---
    function handleActiveInput() {
        if (!config.enableRtl) return;
        const activeEl = document.activeElement;
        if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable)) {
            const text = activeEl.isContentEditable ? activeEl.textContent : activeEl.value;
            if (rtlLangRegex.test(text)) {
                activeEl.dir = 'rtl';
            } else {
                activeEl.removeAttribute('dir');
            }
        }
    }

    // --- 工具函数与事件监听 ---
    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }
    const observer = new MutationObserver(debounce(() => {
        handleActiveInput();
        runFullScan(document.body);
    }, config.debounceTime));

    function init() {
        applyStyles(document.head);
        document.querySelectorAll('*').forEach(el => { if (el.shadowRoot) applyStyles(el.shadowRoot); });
        if (document.body) {
             runFullScan(document.body);
             observer.observe(document.body, { childList: true, subtree: true, characterData: true });
        }
        document.addEventListener('keyup', handleActiveInput, true);
    }

    // --- 设置菜单 (完整版) ---
    function showSettingsDialog() {

        if (document.getElementById('gm-font-config-container')) return;
        const dialogHTML = ` <div id="gm-font-config-overlay"></div> <div id="gm-font-config-dialog"> <h2>全局字体与RTL配置</h2> <div class="gm-config-form"> <div class="gm-form-group"> <label for="gm-font-name">要应用的字体名称:</label> <input type="text" id="gm-font-name" placeholder="例如: ukij ekran"> </div> <div class="gm-form-group gm-checkbox-group"> <input type="checkbox" id="gm-enable-font-replace"> <label for="gm-enable-font-replace">启用全局字体替换</label> </div> <div class="gm-form-group gm-checkbox-group"> <input type="checkbox" id="gm-enable-rtl"> <label for="gm-enable-rtl">启用维吾尔文自动RTL</label> </div> <div class="gm-form-group"> <label for="gm-excluded-sites">排除列表 (网站域名, 用逗号分隔):</label> <textarea id="gm-excluded-sites" rows="3"></textarea> </div> </div> <div class="gm-dialog-buttons"> <button id="gm-save-btn" class="gm-btn gm-btn-primary">保存并刷新</button> <button id="gm-cancel-btn" class="gm-btn">取消</button> </div> </div>`; const dialogCSS = ` #gm-font-config-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.6); z-index: 999999998; } #gm-font-config-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 450px; max-width: 90%; background-color: #fff; color: #333; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); z-index: 999999999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 20px 25px; box-sizing: border-box; } #gm-font-config-dialog h2 { margin: 0 0 20px; font-size: 20px; text-align: center; color: #1a1a1a; } .gm-form-group { margin-bottom: 15px; } .gm-form-group label { display: block; margin-bottom: 5px; font-weight: 500; font-size: 14px; } .gm-form-group input[type="text"], .gm-form-group textarea { width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; box-sizing: border-box; } .gm-form-group textarea { resize: vertical; } .gm-checkbox-group { display: flex; align-items: center; } .gm-checkbox-group input[type="checkbox"] { margin-right: 10px; width: 16px; height: 16px; } .gm-checkbox-group label { margin-bottom: 0; } .gm-dialog-buttons { margin-top: 25px; text-align: right; } .gm-btn { padding: 10px 18px; border: none; border-radius: 5px; font-size: 14px; font-weight: 500; cursor: pointer; margin-left: 10px; transition: background-color 0.2s; } .gm-btn-primary { background-color: #007bff; color: white; } .gm-btn-primary:hover { background-color: #0056b3; } .gm-btn:not(.gm-btn-primary) { background-color: #e0e0e0; color: #333; } .gm-btn:not(.gm-btn-primary):hover { background-color: #c7c7c7; }`;
        const container = document.createElement('div'); container.id = 'gm-font-config-container'; container.innerHTML = `<style>${dialogCSS}</style>${dialogHTML}`; (document.body || document.documentElement).appendChild(container); document.getElementById('gm-font-name').value = config.fontName; document.getElementById('gm-enable-font-replace').checked = config.enableFontReplace; document.getElementById('gm-enable-rtl').checked = config.enableRtl; document.getElementById('gm-excluded-sites').value = config.excludedSites.join(', ');
        const closeDialog = () => { document.removeEventListener('keydown', handleEscKey); container.remove(); };
        const handleEscKey = (e) => { if (e.key === 'Escape') closeDialog(); };
        document.addEventListener('keydown', handleEscKey);
        document.getElementById('gm-save-btn').addEventListener('click', () => { GM_setValue('fontName', document.getElementById('gm-font-name').value); GM_setValue('enableFontReplace', document.getElementById('gm-enable-font-replace').checked); GM_setValue('enableRtl', document.getElementById('gm-enable-rtl').checked); GM_setValue('excludedSites', document.getElementById('gm-excluded-sites').value); closeDialog(); alert('配置已保存!页面即将刷新以应用新设置。'); window.location.reload(); });
        document.getElementById('gm-cancel-btn').addEventListener('click', closeDialog);
        document.getElementById('gm-font-config-overlay').addEventListener('click', closeDialog);

    }

    // --- 脚本入口 ---
    GM_registerMenuCommand('⚙️ 配置脚本 (v7.6 回归初心版)', showSettingsDialog);
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();