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