Via Css 检验

用于检验Via的Adblock规则中的Css隐藏规则是否有错误,支持自动运行、菜单操作、WebView版本检测、规则数量统计及W3C CSS校验

目前為 2025-03-30 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Via Css 检验
// @namespace    https://viayoo.com/
// @version      3.0
// @license      MIT
// @description  用于检验Via的Adblock规则中的Css隐藏规则是否有错误,支持自动运行、菜单操作、WebView版本检测、规则数量统计及W3C CSS校验
// @author       Copilot & Grok & nobody
// @run-at       document-end
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM.xmlHttpRequest
// @connect      jigsaw.w3.org
// @require      https://cdn.jsdelivr.net/npm/[email protected]/js/lib/beautify-css.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/csstree.min.js
// ==/UserScript==

(function() {
    'use strict';

    function getCssFileUrl() {
        const currentHost = window.location.hostname;
        return `https://${currentHost}/via_inject_blocker.css`;
    }

    function formatCssWithJsBeautify(rawCss) {
        try {
            const formatted = css_beautify(rawCss, {
                indent_size: 2,
                selector_separator_newline: true
            });
            console.log('格式化后的CSS:', formatted);
            return formatted;
        } catch (error) {
            console.error(`CSS格式化失败:${error.message}`);
            return null;
        }
    }

    function getWebViewVersion() {
        const ua = navigator.userAgent;
        console.log('User-Agent:', ua);
        const patterns = [
            /Chrome\/([\d.]+)/i,
            /wv\).*?Version\/([\d.]+)/i,
            /Android.*?Version\/([\d.]+)/i
        ];

        for (let pattern of patterns) {
            const match = ua.match(pattern);
            if (match) {
                console.log('匹配到的版本:', match[1]);
                return match[1];
            }
        }
        return null;
    }

    function checkPseudoClassSupport(cssContent) {
        const pseudoClasses = [
            // 原生 CSS 伪类
            { name: ':hover', minVersion: 37 },
            { name: ':focus', minVersion: 37 },
            { name: ':active', minVersion: 37 },
            { name: ':nth-child', minVersion: 37 },
            { name: ':not', minVersion: 37 },
            { name: ':where', minVersion: 88 },
            { name: ':is', minVersion: 88 },
            { name: ':has', minVersion: 105 },

            // AdGuard 和 uBlock Origin 扩展伪类
            { name: ':contains', minVersion: 37, isExtended: true },
            { name: ':has-text', minVersion: 37, isExtended: true },
            { name: ':matches-css', minVersion: 37, isExtended: true },
            { name: ':matches-path', minVersion: 37, isExtended: true },
            { name: ':matches-css-before', minVersion: 37, isExtended: true },
            { name: ':matches-css-after', minVersion: 37, isExtended: true },
            { name: ':if', minVersion: 37, isExtended: true },
            { name: ':if-not', minVersion: 37, isExtended: true },
            { name: ':xpath', minVersion: 37, isExtended: true },
            { name: ':nth-ancestor', minVersion: 37, isExtended: true },
            { name: ':upward', minVersion: 37, isExtended: true },
            { name: ':remove', minVersion: 37, isExtended: true }
        ];
        const webviewVersion = getWebViewVersion();
        let unsupportedPseudo = [];

        if (!webviewVersion) {
            return "无法检测到WebView或浏览器内核版本";
        }

        const versionNum = parseFloat(webviewVersion);
        console.log('检测到的WebView版本:', versionNum);

        pseudoClasses.forEach(pseudo => {
            if (cssContent.includes(pseudo.name)) {
                if (versionNum < pseudo.minVersion) {
                    unsupportedPseudo.push(`${pseudo.name} (需要版本 ${pseudo.minVersion}+${pseudo.isExtended ? ',仅AdGuard/uBlock支持' : ''})`);
                } else if (pseudo.isExtended) {
                    unsupportedPseudo.push(`\n ⚠️ ${pseudo.name} (仅AdGuard/uBlock支持,非原生CSS)`);
                }
            }
        });

        return unsupportedPseudo.length > 0 ?
            `当前版本(${webviewVersion})存在以下伪类问题:${unsupportedPseudo.join(', ')}` :
            `当前版本(${webviewVersion})支持所有使用的伪类`;
    }

    function countCssRules(formattedCss) {
        if (!formattedCss) return 0;

        try {
            const ast = csstree.parse(formattedCss);
            let count = 0;

            csstree.walk(ast, (node) => {
                if (node.type === 'Rule' && node.prelude && node.prelude.type === 'SelectorList') {
                    const selectors = node.prelude.children.size;
                    count += selectors;
                }
            });
            console.log('计算得到的规则总数:', count);
            return count;
        } catch (e) {
            console.error('CSS规则计数失败:', e);
            return 0;
        }
    }

    function getCssPerformance(totalCssRules) {
        if (totalCssRules <= 5000) {
            return '✅CSS规则数量正常,可以流畅运行';
        } else if (totalCssRules <= 7000) {
            return '❓CSS规则数量较多,可能会导致设备运行缓慢';
        } else if (totalCssRules < 9999) {
            return '⚠️CSS规则数量接近上限,可能明显影响设备性能';
        } else {
            return '🆘CSS规则数量过多,建议调整订阅规则';
        }
    }

    function truncateErrorLine(errorLine, maxLength = 150) {
        return errorLine.length > maxLength ? errorLine.substring(0, maxLength) + "..." : errorLine;
    }

    async function fetchAndFormatCss() {
        const url = getCssFileUrl();
        console.log('尝试获取CSS文件:', url);
        try {
            const response = await fetch(url, {
                cache: 'no-store'
            });
            if (!response.ok) throw new Error(`HTTP状态: ${response.status}`);
            const text = await response.text();
            console.log('原始CSS内容:', text);
            return text;
        } catch (error) {
            console.error(`获取CSS失败:${error.message}`);
            return null;
        }
    }

    function translateErrorMessage(englishMessage) {
        const translations = {
            "Identifier is expected": "需要标识符",
            "Unexpected end of input": "输入意外结束",
            "Selector is expected": "需要选择器",
            "Invalid character": "无效字符",
            "Unexpected token": "意外的标记",
            '"]" is expected': '需要 "]"',
            '"{" is expected': '需要 "{"',
            'Unclosed block': '未闭合的块',
            'Unclosed string': '未闭合的字符串',
            'Property is expected': '需要属性名',
            'Value is expected': '需要属性值',
            "Percent sign is expected": "需要百分号 (%)",
            'Attribute selector (=, ~=, ^=, $=, *=, |=) is expected': '需要属性选择器运算符(=、~=、^=、$=、*=、|=)',
            'Semicolon is expected': '需要分号 ";"',
            'Number is expected': '需要数字',
            'Colon is expected': '需要冒号 ":"'
        };
        return translations[englishMessage] || `${englishMessage}`;
    }

    function validateCss(rawCss, formattedCss, isAutoRun = false) {
        if (!formattedCss) return;

        let hasError = false;
        const errors = [];
        const lines = formattedCss.split('\n');
        const totalCssRules = countCssRules(formattedCss);
        const cssPerformance = getCssPerformance(totalCssRules);
        const pseudoSupport = checkPseudoClassSupport(rawCss);

        try {
            csstree.parse(formattedCss, {
                onParseError(error) {
                    hasError = true;
                    const errorLine = lines[error.line - 1] || "无法提取错误行";
                    const truncatedErrorLine = truncateErrorLine(errorLine);
                    const translatedMessage = translateErrorMessage(error.message);

                    errors.push(`
CSS 解析错误:
- 位置:第 ${error.line} 行
- 错误信息:${translatedMessage}
- 错误片段:${truncatedErrorLine}
                    `.trim());
                }
            });

            const resultMessage = `
CSS验证结果:
- 规则总数:${totalCssRules}
- 性能评价:${cssPerformance}
- 伪类支持:${pseudoSupport}
${hasError ? '\n发现错误:\n' + errors.join('\n\n') : '\n未发现语法错误'}
            `.trim();

            if (isAutoRun && hasError) {
                alert(resultMessage);
            } else if (!isAutoRun) {
                alert(resultMessage);
            }
        } catch (error) {
            const translatedMessage = translateErrorMessage(error.message);
            alert(`CSS验证失败:${translatedMessage}`);
        }
    }

    async function validateCssWithW3C(cssText) {
        const validatorUrl = "https://jigsaw.w3.org/css-validator/validator";
        try {
            const formData = new FormData();
            formData.append("text", cssText);
            formData.append("profile", "css3");
            formData.append("output", "json");

            const response = await fetch(validatorUrl, {
                method: "POST",
                body: formData,
            });

            if (!response.ok) {
                alert(`W3C校验服务请求失败:状态码 ${response.status}`);
                return;
            }

            const result = await response.json();
            console.log("W3C Validator返回的JSON:", result);

            if (result && result.cssvalidation) {
                const errors = result.cssvalidation.errors || [];
                const warnings = result.cssvalidation.warnings || [];

                if (errors.length > 0) {
                    const errorDetails = errors.map(err => {
                        const line = err.line || "未知行号";
                        const message = err.message || "未知错误";
                        const context = err.context || "无上下文";
                        return `行 ${line}: ${message} (上下文: ${context})`;
                    }).join("\n\n");
                    alert(`W3C校验发现 ${errors.length} 个CSS错误:\n\n${errorDetails}`);
                } else if (warnings.length > 0) {
                    const warningDetails = warnings.map(warn => {
                        const line = warn.line || "未知行号";
                        const message = warn.message || "未知警告";
                        return `行 ${line}: ${message}`;
                    }).join("\n\n");
                    alert(`W3C校验未发现错误,但有 ${warnings.length} 个警告:\n\n${warningDetails}`);
                } else {
                    alert("W3C CSS校验通过,未发现错误或警告!");
                }
            } else {
                alert("W3C校验服务返回无效结果,请查看控制台!");
            }
        } catch (e) {
            console.error("W3C校验请求失败:", e);
            alert("W3C校验请求失败,请检查控制台日志!");
        }
    }

    async function autoRunCssValidation() {
        const rawCss = await fetchAndFormatCss();
        if (rawCss) {
            const formattedCss = formatCssWithJsBeautify(rawCss);
            if (formattedCss) {
                validateCss(rawCss, formattedCss, true);
            }
        }
    }

    async function checkCssFileWithW3C() {
        const cssFileUrl = getCssFileUrl();
        try {
            const response = await fetch(cssFileUrl, {
                method: 'GET',
                cache: 'no-store'
            });
            if (!response.ok) {
                alert(`无法加载CSS文件: ${cssFileUrl} (状态码: ${response.status})`);
                return;
            }

            const cssText = await response.text();
            if (!cssText.trim()) {
                alert("CSS文件为空!");
                return;
            }

            console.log("要校验的CSS内容:", cssText);
            await validateCssWithW3C(cssText);
        } catch (err) {
            console.error("获取CSS文件失败:", err);
            alert("获取CSS文件失败,请检查控制台日志!");
        }
    }

    function initializeScript() {
        const isAutoRunEnabled = GM_getValue("autoRun", true);

        GM_registerMenuCommand(isAutoRunEnabled ? "关闭自动运行" : "开启自动运行", () => {
            GM_setValue("autoRun", !isAutoRunEnabled);
            alert(`自动运行已${isAutoRunEnabled ? "关闭" : "开启"}!`);
        });

        GM_registerMenuCommand("验证CSS文件(本地)", async () => {
            const rawCss = await fetchAndFormatCss();
            if (rawCss) {
                const formattedCss = formatCssWithJsBeautify(rawCss);
                if (formattedCss) {
                    validateCss(rawCss, formattedCss, false);
                }
            }
        });

        GM_registerMenuCommand("验证CSS文件(W3C)", () => {
            checkCssFileWithW3C();
        });

        if (isAutoRunEnabled) {
            autoRunCssValidation();
        }
    }

    initializeScript();
})();