Via Css隐藏规则日志

格式化CSS规则,检测哪些规则生效,并输出匹配日志。

目前为 2025-03-20 提交的版本。查看 最新版本

// ==UserScript==
// @name         Via Css隐藏规则日志
// @namespace    https://viayoo.com/
// @version      2.3.4
// @license      MIT
// @description  格式化CSS规则,检测哪些规则生效,并输出匹配日志。
// @author       Copilot & Grok
// @run-at       document-start
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://cdn.jsdelivr.net/npm/[email protected]/js/lib/beautify-css.js
// ==/UserScript==

(function() {
    'use strict';

    const BUTTON_STORAGE = {
        ENABLED: 'floatingButtonEnabled',
        LEFT: 'floatingButtonLeft',
        TOP: 'floatingButtonTop'
    };
    const CSS_FILE_PATH = '/via_inject_blocker.css';
    const LONG_PRESS_THRESHOLD = 500; // 长按阈值,单位:毫秒
    const log = (msg, type = 'log') => console[type](`[Via CSS Logger] ${msg}`);

    const createStyledElement = (tag, styles, text) => {
        const el = document.createElement(tag);
        Object.assign(el.style, styles);
        if (text) el.textContent = text;
        return el;
    };

    const formatCss = rawCss => {
        try {
            return css_beautify(rawCss, {
                indent_size: 2,
                selector_separator_newline: true
            });
        } catch (e) {
            log(`CSS格式化失败:${e.message}`, 'error');
            return null;
        }
    };

    const splitSelectors = selectorText => {
        const selectors = [];
        let current = '',
            inAttr = false,
            quote = null;
        for (let i = 0; i < selectorText.length; i++) {
            const char = selectorText[i];
            if (inAttr) {
                current += char;
                if (char === quote) inAttr = false;
            } else if (char === '"' || char === "'") {
                inAttr = true;
                quote = char;
                current += char;
            } else if (char === ',' && !inAttr) {
                if (current.trim()) selectors.push(current.trim());
                current = '';
            } else {
                current += char;
            }
        }
        if (current.trim()) selectors.push(current.trim());
        return selectors;
    };

    const extractValidSelectors = rule => {
        if (!rule.selectorText) return [];
        return splitSelectors(rule.selectorText).filter(selector => {
            try {
                document.querySelector(selector);
                return true;
            } catch {
                return false;
            }
        });
    };

    const checkActiveRules = sheet => {
        if (!sheet?.cssRules) return [];
        const activeRules = [];
        for (const rule of sheet.cssRules) {
            if (rule.selectorText) {
                extractValidSelectors(rule).forEach(selector => {
                    const elements = document.querySelectorAll(selector);
                    if (elements.length) {
                        activeRules.push({
                            selector,
                            count: elements.length
                        });
                    }
                });
            }
        }
        return activeRules;
    };

    const checkCssFile = async () => {
        const cssUrl = `https://${window.location.hostname}${CSS_FILE_PATH}`;
        try {
            const response = await fetch(cssUrl);
            if (!response.ok) throw new Error(`状态码: ${response.status}`);

            const rawCss = await response.text();
            if (!rawCss.trim()) throw new Error('CSS文件为空');

            const formattedCss = formatCss(rawCss);
            if (!formattedCss) throw new Error('CSS格式化失败');

            const style = createStyledElement('style');
            style.textContent = formattedCss;
            document.head.appendChild(style);

            const activeRules = checkActiveRules(style.sheet);
            document.head.removeChild(style);

            const message = activeRules.length ?
                `检测完成!共有 ${activeRules.length} 条规则生效:\n\n` +
                activeRules.map((r, i) => `${i + 1}. 规则: ##${r.selector}\n   匹配数: ${r.count}\n`).join('\n') :
                '没有发现生效的CSS规则!';
            alert(message);
        } catch (e) {
            log(`CSS检查失败:${e.message}`, 'error');
            alert(`检查CSS文件失败:${e.message}\nURL: ${cssUrl}`);
        }
    };

    const createFloatingButton = () => {
        if (window.self !== window.top) {
            log('当前页面是iframe,跳过按钮创建', 'warn');
            return;
        }

        const button = createStyledElement('div', {
            position: 'fixed',
            zIndex: '10000',
            width: '70px',
            height: '35px',
            backgroundColor: '#2d89ef',
            color: 'white',
            borderRadius: '5px',
            textAlign: 'center',
            lineHeight: '35px',
            fontSize: '14px',
            boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
            cursor: 'pointer',
            opacity: '0.9',
            transition: 'opacity 0.3s, transform 0.3s',
            touchAction: 'none'
        }, 'CSS日志');

        const defaultLeft = window.innerWidth - 100;
        const defaultTop = window.innerHeight - 100;
        button.style.left = `${GM_getValue(BUTTON_STORAGE.LEFT, defaultLeft)}px`;
        button.style.top = `${GM_getValue(BUTTON_STORAGE.TOP, defaultTop)}px`;

        document.body.appendChild(button);
        log('悬浮按钮已创建');

        let isDragging = false,
            startX, startY, startLeft, startTop, touchStartTime;

        button.addEventListener('touchstart', e => {
            e.preventDefault();
            touchStartTime = Date.now();
            isDragging = false;
            const touch = e.touches[0];
            startX = touch.clientX;
            startY = touch.clientY;
            startLeft = parseInt(button.style.left, 10) || 0;
            startTop = parseInt(button.style.top, 10) || 0;
        });

        button.addEventListener('touchmove', e => {
            e.preventDefault();
            const touch = e.touches[0];
            const deltaX = touch.clientX - startX;
            const deltaY = touch.clientY - startY;

            // 如果移动发生,且按下时间超过阈值,视为拖动
            if (Date.now() - touchStartTime >= LONG_PRESS_THRESHOLD) {
                isDragging = true;
                const newLeft = startLeft + deltaX;
                const newTop = startTop + deltaY;
                const rect = button.getBoundingClientRect();
                button.style.left = `${Math.max(0, Math.min(newLeft, window.innerWidth - rect.width))}px`;
                button.style.top = `${Math.max(0, Math.min(newTop, window.innerHeight - rect.height))}px`;
            }
        });

        button.addEventListener('touchend', e => {
            e.preventDefault();
            const touchDuration = Date.now() - touchStartTime;

            if (isDragging && touchDuration >= LONG_PRESS_THRESHOLD) {
                // 长按拖动结束
                const rect = button.getBoundingClientRect();
                const newLeft = rect.left + rect.width / 2 < window.innerWidth / 2 ? 0 : window.innerWidth - rect.width;
                button.style.left = `${newLeft}px`;
                GM_setValue(BUTTON_STORAGE.LEFT, newLeft);
                GM_setValue(BUTTON_STORAGE.TOP, parseInt(button.style.top, 10));
            } else if (touchDuration < LONG_PRESS_THRESHOLD) {
                // 短按视为点击
                checkCssFile();
            }
        });

        return button;
    };

    const ensureButtonExists = () => {
        if (!document.querySelector("div[style*='CSS日志']")) {
            log('创建悬浮按钮');
            createFloatingButton();
        } else {
            log('悬浮按钮已存在');
        }
    };

    const resetButtonPosition = () => {
        const defaultLeft = window.innerWidth - 100;
        const defaultTop = window.innerHeight - 100;
        GM_setValue(BUTTON_STORAGE.LEFT, defaultLeft);
        GM_setValue(BUTTON_STORAGE.TOP, defaultTop);

        const button = document.querySelector("div[style*='CSS日志']");
        if (button) {
            button.style.left = `${defaultLeft}px`;
            button.style.top = `${defaultTop}px`;
        }
        alert('悬浮按钮位置已重置至默认位置!');
    };


    const init = () => {
        const isButtonEnabled = GM_getValue(BUTTON_STORAGE.ENABLED, false);

        GM_registerMenuCommand(
            isButtonEnabled ? '关闭悬浮按钮' : '开启悬浮按钮',
            () => {
                GM_setValue(BUTTON_STORAGE.ENABLED, !isButtonEnabled);
                alert(`悬浮按钮已${isButtonEnabled ? '关闭' : '开启'}!`);
                location.reload();
            }
        );

        GM_registerMenuCommand('检测CSS隐藏规则', checkCssFile);
        GM_registerMenuCommand('重置悬浮按钮位置', resetButtonPosition);

        if (isButtonEnabled) {
            document.readyState === 'loading' ?
                document.addEventListener('DOMContentLoaded', ensureButtonExists) :
                ensureButtonExists();
        }
    };

    init();
})();