Greasy Fork

Greasy Fork is available in English.

ChromaFlow1

网页文字渐变色辅助阅读:终极健壮版。引入 Shadow DOM 样式穿透与安全边界跨越,完美兼容 MSN Web Components 与 AI Studio 深度组件,彻底解决断层与崩溃问题。

当前为 2026-04-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChromaFlow1
// @namespace    http://tampermonkey.net/
// @version      14.0
// @description  网页文字渐变色辅助阅读:终极健壮版。引入 Shadow DOM 样式穿透与安全边界跨越,完美兼容 MSN Web Components 与 AI Studio 深度组件,彻底解决断层与崩溃问题。
// @author       Lain1984 & Refactored via AI
// @license      MIT
// @match        *://*/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    if (typeof CSS === 'undefined' || !CSS.highlights) {
        console.warn('ChromaFlow: 您的浏览器不支持 CSS Custom Highlight API,脚本无法运行。');
        return;
    }

    // ==========================================
    // 模块 1:全局配置
    // ==========================================
    const Config = {
        enabled: true,
        debug: false,
        bucketCount: 20,
        tolerance: 16,
        wordRegex: /(?:\p{Emoji_Presentation}|\p{Extended_Pictographic})|\p{Script=Han}|[a-zA-Z0-9_’'.-]+|[^\s\p{Script=Han}a-zA-Z0-9_’'.-]+/gu,

        selectors: {
            targets: 'p, li, blockquote, dd, dt, h1, h2, h3, h4, h5, h6, ms-cmark-node, .text-base, .markdown-body, .prose, [data-testid="tweetText"]',
            ignores: 'code, pre, kbd, button, input, textarea, select, [contenteditable="true"], .inline-code, .immersive-translate-loading-spinner',
            shadowHosts: ''
        },

        themes: {
            light: { c1: [210, 0, 0], mid: [30, 30, 30], c2: [0, 0, 210] },
            dark: { c1: [255, 100, 100], mid: [220, 220, 220], c2: [100, 150, 255] }
        },

        initAdapters() {
            const currentHost = window.location.hostname;
            const adapters = [
                {
                    name: "MSN & Bing News",
                    match: /msn\.com|bing\.com/i,
                    targets: 'p, h2, h3, h4, h5, h6, blockquote, li',
                    ignores: 'views-native-ad, fluent-button, .ad-slot-placeholder, .article-cont-read-container, .continue-reading-slot',
                    tolerance: 15,
                    shadowHosts: 'cp-article' // 关键:MSN 使用 Web Components
                }
            ];

            for (let adapter of adapters) {
                if (adapter.match.test(currentHost)) {
                    if (adapter.targets) this.selectors.targets = adapter.targets;
                    if (adapter.ignores) this.selectors.ignores += `, ${adapter.ignores}`;
                    if (adapter.tolerance) this.tolerance = adapter.tolerance;
                    if (adapter.shadowHosts) this.selectors.shadowHosts = adapter.shadowHosts;
                    break;
                }
            }
        }
    };

    // ==========================================
    // 模块 2:颜色计算与 CSS 注入引擎 (核心突破:Shadow 穿透)
    // ==========================================
    class ColorEngine {
        constructor() {
            this.highlightsMap = {};
            this.baseCssStr = '';
            this.initPalettes();
        }

        interpolate(color1, color2, factor) {
            return [
                Math.round(color1[0] + factor * (color2[0] - color1[0])),
                Math.round(color1[1] + factor * (color2[1] - color1[1])),
                Math.round(color1[2] + factor * (color2[2] - color1[2]))
            ];
        }

        getGradientRGB(theme, progress) {
            let rgb = progress < 0.5
                ? this.interpolate(theme.c1, theme.mid, progress / 0.5)
                : this.interpolate(theme.mid, theme.c2, (progress - 0.5) / 0.5);
            return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
        }

        initPalettes() {
            for (let i = 0; i <= Config.bucketCount; i++) {
                const progress = i / Config.bucketCount;
                ['light', 'dark'].forEach(theme => {
                    const bucketName = `cf-${theme}-${i}`;
                    this.highlightsMap[bucketName] = new Highlight();
                    CSS.highlights.set(bucketName, this.highlightsMap[bucketName]);
                    this.baseCssStr += `::highlight(${bucketName}) { color: ${this.getGradientRGB(Config.themes[theme], progress)} !important; }\n`;
                });
            }
            // 主文档注入
            this.injectCSS(document);
        }

        // 动态将样式注入到目标作用域 (突破 Web Components 样式隔离)
        injectCSS(root) {
            const id = 'chromaflow-styles';
            if (root.getElementById && root.getElementById(id)) return;
            if (root.querySelector && root.querySelector(`#${id}`)) return;

            const style = document.createElement('style');
            style.id = id;
            style.textContent = this.baseCssStr;

            if (root === document) {
                if (typeof GM_addStyle !== 'undefined') {
                    GM_addStyle(this.baseCssStr); // 兼容某些油猴特性
                } else {
                    document.head.appendChild(style);
                }
            } else {
                root.appendChild(style); // 注入到 ShadowRoot
            }
        }

        clearAll() {
            Object.values(this.highlightsMap).forEach(hl => hl.clear());
        }

        isLightText(colorStr) {
            if (!colorStr) return false;
            const rgbMatch = colorStr.match(/\d+/g);
            if (!rgbMatch || rgbMatch.length < 3) return false;
            const [r, g, b] = rgbMatch.map(Number);
            return (((r * 299) + (g * 587) + (b * 114)) / 1000) >= 128;
        }

        assignRangeToBucket(range, bucketName, oldBucketName) {
            if (oldBucketName === bucketName) return;
            if (oldBucketName && this.highlightsMap[oldBucketName]) {
                this.highlightsMap[oldBucketName].delete(range);
            }
            if (bucketName && this.highlightsMap[bucketName]) {
                this.highlightsMap[bucketName].add(range);
            }
        }
    }

    // ==========================================
    // 模块 3:核心文本解析与聚类 (健壮节点穿越)
    // ==========================================
    class TextProcessor {
        constructor(colorEngine) {
            this.colorEngine = colorEngine;
            this.nodeRangesMap = new WeakMap();
            this.processedBlocks = new WeakMap();
            this.blockDirectionState = new WeakMap();
        }

        clearState(block) {
            this.processedBlocks.set(block, false);
        }

        cleanupNode(node) {
            const entries = this.nodeRangesMap.get(node);
            if (entries) {
                entries.forEach(entry => this.colorEngine.assignRangeToBucket(entry.range, null, entry.bucket));
                this.nodeRangesMap.delete(node);
            }
        }

        // 安全地向上跨越层级和影子DOM查找前驱状态
        getPreviousDirectionState(block) {
            let current = block;
            let depth = 0;

            while (current && depth < 12) {
                let sibling = current.previousElementSibling;
                while (sibling) {
                    if (sibling.querySelectorAll) {
                        const targets = sibling.querySelectorAll(Config.selectors.targets);
                        if (targets.length > 0) {
                            for (let i = targets.length - 1; i >= 0; i--) {
                                if (this.blockDirectionState.has(targets[i])) {
                                    return this.blockDirectionState.get(targets[i]);
                                }
                            }
                        }
                    }
                    if (sibling.matches && sibling.matches(Config.selectors.targets) && this.blockDirectionState.has(sibling)) {
                        return this.blockDirectionState.get(sibling);
                    }
                    sibling = sibling.previousElementSibling;
                }

                // 没找到兄弟?往上爬。如果碰到了 ShadowRoot 边界,通过 host 跨越到外层 Light DOM 继续找
                if (current.parentElement) {
                    current = current.parentElement;
                } else if (current.getRootNode && current.getRootNode() instanceof ShadowRoot) {
                    current = current.getRootNode().host;
                } else {
                    break;
                }
                depth++;
            }
            return 0;
        }

        processBlock(block, isResize = false) {
            if (!Config.enabled || block.closest(Config.selectors.ignores)) return;
            if (!block.isConnected) return; // 防御断开的 DOM

            if (block.offsetWidth === 0 && block.offsetHeight === 0) {
                if (window.getComputedStyle(block).display !== 'contents') return;
            }

            const originalColor = window.getComputedStyle(block).color;
            const themePrefix = this.colorEngine.isLightText(originalColor) ? 'cf-dark-' : 'cf-light-';

            const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, {
                acceptNode: node => {
                    if (!node.nodeValue.trim()) return NodeFilter.FILTER_SKIP;
                    if (node.parentNode && node.parentNode.closest(Config.selectors.ignores)) return NodeFilter.FILTER_REJECT;
                    return NodeFilter.FILTER_ACCEPT;
                }
            });

            const textNodes = [];
            let currentNode;
            while (currentNode = walker.nextNode()) textNodes.push(currentNode);

            if (textNodes.length === 0) {
                this.processedBlocks.set(block, true);
                return;
            }

            let allWords = [];

            textNodes.forEach(node => {
                let wordEntries = this.nodeRangesMap.get(node);

                if (!wordEntries || isResize) {
                    this.cleanupNode(node);
                    wordEntries = [];
                    const text = node.nodeValue;
                    Config.wordRegex.lastIndex = 0;
                    let match;
                    // 安全判断链接 (由于使用TreeWalker,parentNode必为Element)
                    const isLink = !!(node.parentNode.closest('a'));

                    while ((match = Config.wordRegex.exec(text)) !== null) {
                        try {
                            const range = new Range();
                            range.setStart(node, match.index);
                            range.setEnd(node, match.index + match[0].length);
                            wordEntries.push({ range, bucket: null, isLink });
                        } catch(e) {}
                    }
                    this.nodeRangesMap.set(node, wordEntries);
                }

                wordEntries.forEach(entry => {
                    if(!entry.range.startContainer.isConnected) return;
                    const rects = entry.range.getClientRects();
                    if (rects.length === 0) return;
                    const rect = rects[0];
                    if (rect.width === 0 || rect.height === 0) return;

                    allWords.push({
                        entry: entry,
                        x: rect.left,
                        y: rect.top + rect.height / 2
                    });
                });
            });

            if (allWords.length === 0) {
                this.processedBlocks.set(block, true);
                return;
            }

            let directionToggle = this.getPreviousDirectionState(block);

            allWords.sort((a, b) => a.y - b.y || a.x - b.x);
            let lines = [];
            let currentLine = [allWords[0]];
            let currentLineY = allWords[0].y;

            for (let i = 1; i < allWords.length; i++) {
                let word = allWords[i];
                if (Math.abs(word.y - currentLineY) < Config.tolerance) {
                    currentLine.push(word);
                    currentLineY = (currentLineY * (currentLine.length - 1) + word.y) / currentLine.length;
                } else {
                    lines.push(currentLine);
                    currentLine = [word];
                    currentLineY = word.y;
                }
            }
            lines.push(currentLine);

            lines.forEach((lineWords) => {
                const lineLength = lineWords.length;
                const isOdd = directionToggle % 2 !== 0;

                lineWords.forEach((wordObj, wordIndex) => {
                    if (wordObj.entry.isLink) {
                        this.colorEngine.assignRangeToBucket(wordObj.entry.range, null, wordObj.entry.bucket);
                        wordObj.entry.bucket = null;
                        return;
                    }

                    let progress = lineLength > 1 ? wordIndex / (lineLength - 1) : 0;
                    if (isOdd) progress = 1 - progress;

                    const bucketIndex = Math.min(Config.bucketCount, Math.max(0, Math.round(progress * Config.bucketCount)));
                    const newBucket = `${themePrefix}${bucketIndex}`;

                    this.colorEngine.assignRangeToBucket(wordObj.entry.range, newBucket, wordObj.entry.bucket);
                    wordObj.entry.bucket = newBucket;
                });

                if (lineLength > 1) directionToggle++;
            });

            this.blockDirectionState.set(block, directionToggle);
            this.processedBlocks.set(block, true);
        }
    }

    // ==========================================
    // 模块 4:生命周期与事件调度 (安全隔离探测)
    // ==========================================
    class ObserverManager {
        constructor(processor, colorEngine) {
            this.processor = processor;
            this.colorEngine = colorEngine; // 引入 Engine 用于 CSS 注入
            this.pendingBlocks = new Set();
            this.processTimer = null;
            this.observedShadowHosts = new WeakSet();
            this.blockDisplayCache = new WeakMap();

            this.initViewportObserver();
            this.initMutationObserver();
            this.initResizeObserver();
            this.initFallbackScanner();
        }

        isTrueBlockLevel(element) {
            const tag = element.tagName.toLowerCase();
            if (['p', 'li', 'blockquote', 'dd', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div'].includes(tag)) {
                return true;
            }
            if (this.blockDisplayCache.has(element)) return this.blockDisplayCache.get(element);

            if (!element.isConnected) return false;
            const display = window.getComputedStyle(element).display;
            const isBlock = !['inline', 'inline-block', 'contents', 'none'].includes(display);
            this.blockDisplayCache.set(element, isBlock);
            return isBlock;
        }

        // 安全获取逻辑块:过滤 ShadowRoot 导致的 TypeError
        getEffectiveBlock(node) {
            let current = node.nodeType === Node.TEXT_NODE ? node.parentNode : node;
            let fallback = null;

            // 必须验证 Node.ELEMENT_NODE (节点类型 1),防止遇到 DocumentFragment(11) 和 Document(9) 崩溃
            while (current && current.nodeType === Node.ELEMENT_NODE) {
                if (current.tagName === 'BODY' || current.tagName === 'HTML') break;

                // 此时 current.matches 绝对安全
                if (current.matches(Config.selectors.targets) && !current.matches('article, main')) {
                    if (this.isTrueBlockLevel(current)) {
                        return current;
                    } else if (!fallback) {
                        fallback = current;
                    }
                }
                current = current.parentNode;
            }
            return fallback;
        }

        queueBlock(block) {
            if (!Config.enabled) return;
            this.pendingBlocks.add(block);

            if (!this.processTimer) {
                this.processTimer = setTimeout(() => {
                    requestAnimationFrame(() => {
                        this.pendingBlocks.forEach(b => {
                            if (b.isConnected) this.processor.processBlock(b, false);
                        });
                        this.pendingBlocks.clear();
                        this.processTimer = null;
                    });
                }, 150);
            }

            clearTimeout(block._cfRefetchTimer);
            block._cfRefetchTimer = setTimeout(() => {
                if (block.isConnected && Config.enabled) {
                    this.processor.clearState(block);
                    this.processor.processBlock(block, true);
                }
            }, 2500);
        }

        initViewportObserver() {
            this.viewportObserver = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting && Config.enabled) {
                        const block = entry.target;
                        if (!this.processor.processedBlocks.get(block)) this.queueBlock(block);
                    }
                });
            }, { rootMargin: '400px' });
        }

        observeNode(node) {
            if (!this.processor.processedBlocks.has(node)) {
                this.processor.clearState(node);
                this.viewportObserver.observe(node);
            }
        }

        observeShadowRoot(host) {
            if (this.observedShadowHosts.has(host) || !host.shadowRoot) return;
            this.observedShadowHosts.add(host);

            // 【核心突破】向 ShadowRoot 内注入高亮 CSS!
            this.colorEngine.injectCSS(host.shadowRoot);

            const shadowObserver = new MutationObserver(mutations => this.handleMutations(mutations));
            shadowObserver.observe(host.shadowRoot, { childList: true, characterData: true, subtree: true });
        }

        handleMutations(mutations) {
            if (!Config.enabled) return;
            const blocksToProcess = new Set();

            mutations.forEach(m => {
                let target = m.target;

                const block = this.getEffectiveBlock(target);
                if (block) {
                    this.processor.clearState(block);
                    blocksToProcess.add(block);
                    this.observeNode(block);
                }

                m.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE && Config.selectors.shadowHosts && node.matches(Config.selectors.shadowHosts)) {
                        this.observeShadowRoot(node);
                    }
                });
            });

            blocksToProcess.forEach(block => this.queueBlock(block));
        }

        initMutationObserver() {
            this.mutationObserver = new MutationObserver(m => this.handleMutations(m));
            this.mutationObserver.observe(document.body, { childList: true, characterData: true, subtree: true });
        }

        initResizeObserver() {
            let resizeTimer;
            window.addEventListener('resize', () => {
                if (!Config.enabled) return;
                clearTimeout(resizeTimer);
                resizeTimer = setTimeout(() => {
                    this.scanAndObserve(true);
                }, 300);
            });
        }

        scanAndObserve(forceResize = false) {
            if (!Config.enabled) return;

            document.querySelectorAll(Config.selectors.targets).forEach(node => {
                const block = this.getEffectiveBlock(node);
                if (block) {
                    if (forceResize) this.processor.clearState(block);
                    this.observeNode(block);
                    if (forceResize && this.isElementInViewport(block)) this.processor.processBlock(block, true);
                }
            });

            if (Config.selectors.shadowHosts) {
                document.querySelectorAll(Config.selectors.shadowHosts).forEach(host => {
                    this.observeShadowRoot(host); // 注入 CSS 并监听
                    if (host.shadowRoot) {
                        host.shadowRoot.querySelectorAll(Config.selectors.targets).forEach(node => {
                            const block = this.getEffectiveBlock(node);
                            if (block) {
                                if (forceResize) this.processor.clearState(block);
                                this.observeNode(block);
                                if (forceResize && this.isElementInViewport(block)) this.processor.processBlock(block, true);
                            }
                        });
                    }
                });
            }
        }

        isElementInViewport(el) {
            const rect = el.getBoundingClientRect();
            return (rect.top <= (window.innerHeight + 400) && rect.bottom >= -400);
        }

        initFallbackScanner() {
            let scanIntervalTime = 2000;
            const scheduleNextScan = () => {
                setTimeout(() => {
                    if (Config.enabled) this.scanAndObserve();
                    scanIntervalTime = Math.min(scanIntervalTime + 2000, 10000);
                    scheduleNextScan();
                }, scanIntervalTime);
            };
            scheduleNextScan();
        }
    }

    // ==========================================
    // 模块 5:应用入口
    // ==========================================
    class ChromaFlowApp {
        constructor() {
            Config.initAdapters(); // 提取主机信息并加载适配器
            this.colorEngine = new ColorEngine();
            this.processor = new TextProcessor(this.colorEngine);
            this.observer = new ObserverManager(this.processor, this.colorEngine);
            this.initHotkeys();
        }

        initHotkeys() {
            document.addEventListener('keydown', (e) => {
                if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'b' || e.key === 'B')) {
                    e.preventDefault();
                    Config.enabled = !Config.enabled;

                    if (!Config.enabled) {
                        this.colorEngine.clearAll();
                        const clearAllStates = (root) => {
                            root.querySelectorAll(Config.selectors.targets).forEach(b => this.processor.clearState(b));
                        };
                        clearAllStates(document);
                        if (Config.selectors.shadowHosts) {
                            document.querySelectorAll(Config.selectors.shadowHosts).forEach(host => {
                                if (host.shadowRoot) clearAllStates(host.shadowRoot);
                            });
                        }
                    } else {
                        this.observer.scanAndObserve();
                    }
                }
            });
        }
    }

    new ChromaFlowApp();

})();