Via Css 检验
// ==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/js-beautify@1.14.0/js/lib/beautify-css.js
// @require https://cdn.jsdelivr.net/npm/css-tree@2.3.1/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();
})();