Via Css 检验

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

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Via Css 检验
// @namespace    https://viayoo.com/
// @version      3.3
// @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';

    const adblockPseudoClasses = [
        ':contains',
        ':has-text',
        ':matches-css',
        ':matches-css-after',
        ':matches-css-before',
        ':matches-path',
        ':matches-property',
        ':min-text-length',
        ':nth-ancestor',
        ':remove',
        ':style',
        ':upward',
        ':watch-attr',
        ':xpath',
        ':-abp-contains',
        ':-abp-properties',
        ':if',
        ':if-not'
    ];

    function getCssFileUrl() {
        const currentHost = window.location.hostname;
        return `http://${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 = [
            { 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 }
        ];
        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) && versionNum < pseudo.minVersion) {
                unsupportedPseudo.push(`${pseudo.name} (需要版本 ${pseudo.minVersion}+)`);
            }
        });

        return unsupportedPseudo.length > 0 ?
            `当前版本(${webviewVersion})不支持以下伪类:${unsupportedPseudo.join(', ')}` :
            `当前版本(${webviewVersion})支持所有标准伪类`;
    }

    function splitCssAndAdblockRules(formattedCss) {
        const lines = formattedCss.split('\n');
        const standardCss = [];
        const adblockRules = [];

        lines.forEach(line => {
            line = line.trim();
            if (!line) return;
            if (line.startsWith('##') || adblockPseudoClasses.some(pseudo => line.includes(pseudo))) {
                adblockRules.push(line);
            } else {
                standardCss.push(line);
            }
        });

        return {
            standardCss: standardCss.join('\n'),
            adblockRules
        };
    }

    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('计算得到的标准CSS规则总数:', 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;
    }

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

        const {
            standardCss,
            adblockRules
        } = splitCssAndAdblockRules(formattedCss);
        console.log('标准CSS:', standardCss);
        console.log('Adguard/Ublock规则:', adblockRules);

        let hasError = false;
        const errors = [];
        const allLines = formattedCss.split('\n');
        const totalStandardCssRules = countCssRules(standardCss);
        const cssPerformance = getCssPerformance(totalStandardCssRules);
        const pseudoSupport = checkPseudoClassSupport(standardCss);

        if (standardCss) {
            try {
                csstree.parse(standardCss, {
                    onParseError(error) {
                        hasError = true;
                        const standardCssLines = standardCss.split('\n');
                        const errorLine = standardCssLines[error.line - 1] || "无法提取错误行";
                        const originalLineIndex = allLines.indexOf(errorLine);
                        const truncatedErrorLine = truncateErrorLine(errorLine);
                        const translatedMessage = translateErrorMessage(error.message);

                        errors.push(`
CSS解析错误:
- 位置:第 ${originalLineIndex + 1} 行
- 错误信息:${translatedMessage}
- 错误片段:${truncatedErrorLine}
                        `.trim());
                    }
                });
            } catch (error) {
                hasError = true;
                const translatedMessage = translateErrorMessage(error.message);
                errors.push(`标准CSS解析失败:${translatedMessage}`);
            }
        }

        adblockRules.forEach((rule, index) => {
            const originalLineIndex = allLines.indexOf(rule);
            let errorMessage = null;

            const matchedPseudo = adblockPseudoClasses.find(pseudo => rule.includes(pseudo));
            if (matchedPseudo) {
                errorMessage = `非标准伪类 ${matchedPseudo}(AdGuard/uBlock 扩展语法,不支持)`;
            } else if (rule.startsWith('##') && !rule.match(/^##[\w\s\[\]\.,:()]+$/)) {
                errorMessage = '无效的 Adblock 元素隐藏规则';
            }

            if (errorMessage) {
                hasError = true;
                const truncatedRule = truncateErrorLine(rule);
                errors.push(`
CSS解析错误:
- 位置:第 ${originalLineIndex + 1} 行
- 错误信息:${errorMessage}
- 错误片段:${truncatedRule}
                `.trim());
            }
        });

        const resultMessage = `
CSS验证结果:
- 规则总数:${totalStandardCssRules} (标准CSS) + ${adblockRules.length} (Adguard/Ublock拓展规则)
- 性能评价:${cssPerformance}
- 伪类支持:${pseudoSupport}
${errors.length > 0 ? '\n发现错误:\n' + errors.join('\n\n') : '\n未发现语法错误'}
        `.trim();

        if (isAutoRun && errors.length > 0) {
            alert(resultMessage);
        } else if (!isAutoRun) {
            alert(resultMessage);
        }
    }

    async function validateCssWithW3C(cssText) {
        const validatorUrl = "https://jigsaw.w3.org/css-validator/validator";
        try {
            return new Promise((resolve, reject) => {
                GM.xmlHttpRequest({
                    method: "POST",
                    url: validatorUrl,
                    data: `text=${encodeURIComponent(cssText)}&profile=css3&output=json`,
                    headers: {
                        "Content-Type": "application/x-www-form-urlencoded",
                        "Accept": "application/json"
                    },
                    onload: function(response) {
                        try {
                            const result = JSON.parse(response.responseText);
                            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校验服务返回无效结果,请查看控制台!");
                            }
                            resolve();
                        } catch (e) {
                            console.error("W3C校验解析失败:", e);
                            alert("W3C校验解析失败,请检查控制台日志!");
                            reject(e);
                        }
                    },
                    onerror: function(error) {
                        console.error("W3C校验请求失败:", error);
                        alert(`W3C校验请求失败:${error.statusText || '未知错误'} (状态码: ${error.status || '未知'})`);
                        reject(error);
                    }
                });
            });
        } catch (e) {
            console.error("W3C校验请求失败:", e);
            alert(`W3C校验请求失败:${e.message},请检查控制台日志!`);
        }
    }

    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文件失败:${err.message},请检查控制台日志!`);
        }
    }

    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();
})();