Via Css 检验

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

目前為 2025-04-12 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Via Css 检验
  3. // @namespace https://viayoo.com/
  4. // @version 3.2
  5. // @license MIT
  6. // @description 用于检验Via的Adblock规则中的Css隐藏规则是否有错误,支持自动运行、菜单操作、WebView版本检测、规则数量统计及W3C CSS校验
  7. // @author Copilot & Grok & nobody
  8. // @run-at document-end
  9. // @match *://*/*
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM.xmlHttpRequest
  14. // @connect jigsaw.w3.org
  15. // @require https://cdn.jsdelivr.net/npm/js-beautify@1.14.0/js/lib/beautify-css.js
  16. // @require https://cdn.jsdelivr.net/npm/css-tree@2.3.1/dist/csstree.min.js
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. function getCssFileUrl() {
  23. const currentHost = window.location.hostname;
  24. return `https://${currentHost}/via_inject_blocker.css`;
  25. }
  26.  
  27. function formatCssWithJsBeautify(rawCss) {
  28. try {
  29. const formatted = css_beautify(rawCss, {
  30. indent_size: 2,
  31. selector_separator_newline: true
  32. });
  33. console.log('格式化后的CSS:', formatted);
  34. return formatted;
  35. } catch (error) {
  36. console.error(`CSS格式化失败:${error.message}`);
  37. return null;
  38. }
  39. }
  40.  
  41. function getWebViewVersion() {
  42. const ua = navigator.userAgent;
  43. console.log('User-Agent:', ua);
  44. const patterns = [
  45. /Chrome\/([\d.]+)/i,
  46. /wv\).*?Version\/([\d.]+)/i,
  47. /Android.*?Version\/([\d.]+)/i
  48. ];
  49.  
  50. for (let pattern of patterns) {
  51. const match = ua.match(pattern);
  52. if (match) {
  53. console.log('匹配到的版本:', match[1]);
  54. return match[1];
  55. }
  56. }
  57. return null;
  58. }
  59.  
  60. function checkPseudoClassSupport(cssContent) {
  61. const pseudoClasses = [
  62. // 仅包含标准 CSS 伪类
  63. { name: ':hover', minVersion: 37 },
  64. { name: ':focus', minVersion: 37 },
  65. { name: ':active', minVersion: 37 },
  66. { name: ':nth-child', minVersion: 37 },
  67. { name: ':not', minVersion: 37 },
  68. { name: ':where', minVersion: 88 },
  69. { name: ':is', minVersion: 88 },
  70. { name: ':has', minVersion: 105 }
  71. ];
  72. const webviewVersion = getWebViewVersion();
  73. let unsupportedPseudo = [];
  74.  
  75. if (!webviewVersion) {
  76. return "无法检测到WebView或浏览器内核版本";
  77. }
  78.  
  79. const versionNum = parseFloat(webviewVersion);
  80. console.log('检测到的WebView版本:', versionNum);
  81.  
  82. pseudoClasses.forEach(pseudo => {
  83. if (cssContent.includes(pseudo.name) && versionNum < pseudo.minVersion) {
  84. unsupportedPseudo.push(`${pseudo.name} (需要版本 ${pseudo.minVersion}+)`);
  85. }
  86. });
  87.  
  88. return unsupportedPseudo.length > 0 ?
  89. `当前版本(${webviewVersion})不支持以下伪类:${unsupportedPseudo.join(', ')}` :
  90. `当前版本(${webviewVersion})支持所有标准伪类`;
  91. }
  92.  
  93. function splitCssAndAdblockRules(formattedCss) {
  94. const lines = formattedCss.split('\n');
  95. const standardCss = [];
  96. const adblockRules = [];
  97.  
  98. lines.forEach(line => {
  99. line = line.trim();
  100. if (!line) return;
  101. if (line.startsWith('##') || line.includes(':contains') || line.includes(':has-text') || line.includes(':matches-') || line.includes(':-abp-') || line.includes(':if') || line.includes(':xpath')) {
  102. adblockRules.push(line);
  103. } else {
  104. standardCss.push(line);
  105. }
  106. });
  107.  
  108. return {
  109. standardCss: standardCss.join('\n'),
  110. adblockRules
  111. };
  112. }
  113.  
  114. function countCssRules(formattedCss) {
  115. if (!formattedCss) return 0;
  116.  
  117. try {
  118. const ast = csstree.parse(formattedCss);
  119. let count = 0;
  120.  
  121. csstree.walk(ast, (node) => {
  122. if (node.type === 'Rule' && node.prelude && node.prelude.type === 'SelectorList') {
  123. const selectors = node.prelude.children.size;
  124. count += selectors;
  125. }
  126. });
  127. console.log('计算得到的标准CSS规则总数:', count);
  128. return count;
  129. } catch (e) {
  130. console.error('标准CSS规则计数失败:', e);
  131. return 0;
  132. }
  133. }
  134.  
  135. function getCssPerformance(totalCssRules) {
  136. if (totalCssRules <= 5000) {
  137. return '✅CSS规则数量正常,可以流畅运行';
  138. } else if (totalCssRules <= 7000) {
  139. return '❓CSS规则数量较多,可能会导致设备运行缓慢';
  140. } else if (totalCssRules < 9999) {
  141. return '⚠️CSS规则数量接近上限,可能明显影响设备性能';
  142. } else {
  143. return '🆘CSS规则数量过多,建议调整订阅规则';
  144. }
  145. }
  146.  
  147. function truncateErrorLine(errorLine, maxLength = 150) {
  148. return errorLine.length > maxLength ? errorLine.substring(0, maxLength) + "..." : errorLine;
  149. }
  150.  
  151. async function fetchAndFormatCss() {
  152. const url = getCssFileUrl();
  153. console.log('尝试获取CSS文件:', url);
  154. try {
  155. const response = await fetch(url, {
  156. cache: 'no-store'
  157. });
  158. if (!response.ok) throw new Error(`HTTP状态: ${response.status}`);
  159. const text = await response.text();
  160. console.log('原始CSS内容:', text);
  161. return text;
  162. } catch (error) {
  163. console.error(`获取CSS失败:${error.message}`);
  164. return null;
  165. }
  166. }
  167.  
  168. function translateErrorMessage(englishMessage) {
  169. const translations = {
  170. "Identifier is expected": "需要标识符",
  171. "Unexpected end of input": "输入意外结束",
  172. "Selector is expected": "需要选择器",
  173. "Invalid character": "无效字符",
  174. "Unexpected token": "意外的标记",
  175. '"]" is expected': '需要 "]"',
  176. '"{" is expected': '需要 "{"',
  177. 'Unclosed block': '未闭合的块',
  178. 'Unclosed string': '未闭合的字符串',
  179. 'Property is expected': "需要属性名",
  180. 'Value is expected': "需要属性值",
  181. "Percent sign is expected": "需要百分号 (%)",
  182. 'Attribute selector (=, ~=, ^=, $=, *=, |=) is expected': '需要属性选择器运算符(=、~=、^=、$=、*=、|=)',
  183. 'Semicolon is expected': '需要分号 ";"',
  184. 'Number is expected': '需要数字',
  185. 'Colon is expected': '需要冒号 ":"'
  186. };
  187. return translations[englishMessage] || englishMessage;
  188. }
  189.  
  190. async function validateCss(rawCss, formattedCss, isAutoRun = false) {
  191. if (!formattedCss) return;
  192.  
  193. // 分离标准 CSS 和 Adblock 规则
  194. const {
  195. standardCss,
  196. adblockRules
  197. } = splitCssAndAdblockRules(formattedCss);
  198. console.log('标准CSS:', standardCss);
  199. console.log('Adblock规则:', adblockRules);
  200.  
  201. let hasError = false;
  202. const errors = [];
  203. const allLines = formattedCss.split('\n'); // 使用原始格式化 CSS 的行号
  204. const totalStandardCssRules = countCssRules(standardCss);
  205. const cssPerformance = getCssPerformance(totalStandardCssRules);
  206. const pseudoSupport = checkPseudoClassSupport(standardCss); // 只检查标准 CSS
  207.  
  208. // 验证标准 CSS
  209. if (standardCss) {
  210. try {
  211. csstree.parse(standardCss, {
  212. onParseError(error) {
  213. hasError = true;
  214. // 映射到原始 CSS 的行号
  215. const standardCssLines = standardCss.split('\n');
  216. const errorLine = standardCssLines[error.line - 1] || "无法提取错误行";
  217. const originalLineIndex = allLines.indexOf(errorLine);
  218. const truncatedErrorLine = truncateErrorLine(errorLine);
  219. const translatedMessage = translateErrorMessage(error.message);
  220.  
  221. errors.push(`
  222. CSS解析错误:
  223. - 位置:第 ${originalLineIndex + 1}
  224. - 错误信息:${translatedMessage}
  225. - 错误片段:${truncatedErrorLine}
  226. `.trim());
  227. }
  228. });
  229. } catch (error) {
  230. hasError = true;
  231. const translatedMessage = translateErrorMessage(error.message);
  232. errors.push(`标准CSS解析失败:${translatedMessage}`);
  233. }
  234. }
  235.  
  236. // 检查 Adblock 规则(视为语法错误)
  237. const adblockPseudoClasses = [
  238. ':contains',
  239. ':has-text',
  240. ':matches-css',
  241. ':matches-css-after',
  242. ':matches-css-before',
  243. ':matches-path',
  244. ':matches-property',
  245. ':min-text-length',
  246. ':nth-ancestor',
  247. ':remove',
  248. ':style',
  249. ':upward',
  250. ':watch-attr',
  251. ':xpath',
  252. ':-abp-contains',
  253. ':-abp-properties',
  254. ':if',
  255. ':if-not'
  256. ];
  257.  
  258. adblockRules.forEach((rule, index) => {
  259. const originalLineIndex = allLines.indexOf(rule);
  260. let errorMessage = null;
  261.  
  262. // 检查是否包含 Adblock 伪类
  263. const matchedPseudo = adblockPseudoClasses.find(pseudo => rule.includes(pseudo));
  264. if (matchedPseudo) {
  265. errorMessage = `非标准伪类 ${matchedPseudo}(AdGuard/uBlock 扩展语法,不支持)`;
  266. } else if (rule.startsWith('##') && !rule.match(/^##[\w\s\[\]\.,:()]+$/)) {
  267. errorMessage = '无效的 Adblock 元素隐藏规则';
  268. }
  269.  
  270. if (errorMessage) {
  271. hasError = true;
  272. const truncatedRule = truncateErrorLine(rule);
  273. errors.push(`
  274. CSS解析错误:
  275. - 位置:第 ${originalLineIndex + 1}
  276. - 错误信息:${errorMessage}
  277. - 错误片段:${truncatedRule}
  278. `.trim());
  279. }
  280. });
  281.  
  282. const resultMessage = `
  283. CSS验证结果:
  284. - 规则总数:${totalStandardCssRules} (标准CSS) + ${adblockRules.length} (Adblock规则)
  285. - 性能评价:${cssPerformance}
  286. - 伪类支持:${pseudoSupport}
  287. ${errors.length > 0 ? '\n发现错误:\n' + errors.join('\n\n') : '\n未发现语法错误'}
  288. `.trim();
  289.  
  290. if (isAutoRun && errors.length > 0) {
  291. alert(resultMessage);
  292. } else if (!isAutoRun) {
  293. alert(resultMessage);
  294. }
  295. }
  296.  
  297. async function validateCssWithW3C(cssText) {
  298. const validatorUrl = "https://jigsaw.w3.org/css-validator/validator";
  299. try {
  300. return new Promise((resolve, reject) => {
  301. GM.xmlHttpRequest({
  302. method: "POST",
  303. url: validatorUrl,
  304. data: `text=${encodeURIComponent(cssText)}&profile=css3&output=json`,
  305. headers: {
  306. "Content-Type": "application/x-www-form-urlencoded",
  307. "Accept": "application/json"
  308. },
  309. onload: function(response) {
  310. try {
  311. const result = JSON.parse(response.responseText);
  312. console.log("W3C Validator返回的JSON:", result);
  313. if (result && result.cssvalidation) {
  314. const errors = result.cssvalidation.errors || [];
  315. const warnings = result.cssvalidation.warnings || [];
  316. if (errors.length > 0) {
  317. const errorDetails = errors.map(err => {
  318. const line = err.line || "未知行号";
  319. const message = err.message || "未知错误";
  320. const context = err.context || "无上下文";
  321. return `行 ${line}: ${message} (上下文: ${context})`;
  322. }).join("\n\n");
  323. alert(`W3C校验发现 ${errors.length} CSS错误:\n\n${errorDetails}`);
  324. } else if (warnings.length > 0) {
  325. const warningDetails = warnings.map(warn => {
  326. const line = warn.line || "未知行号";
  327. const message = warn.message || "未知警告";
  328. return `行 ${line}: ${message}`;
  329. }).join("\n\n");
  330. alert(`W3C校验未发现错误,但有 ${warnings.length} 个警告:\n\n${warningDetails}`);
  331. } else {
  332. alert("W3C CSS校验通过,未发现错误或警告!");
  333. }
  334. } else {
  335. alert("W3C校验服务返回无效结果,请查看控制台!");
  336. }
  337. resolve();
  338. } catch (e) {
  339. console.error("W3C校验解析失败:", e);
  340. alert("W3C校验解析失败,请检查控制台日志!");
  341. reject(e);
  342. }
  343. },
  344. onerror: function(error) {
  345. console.error("W3C校验请求失败:", error);
  346. alert(`W3C校验请求失败:${error.statusText || '未知错误'} (状态码: ${error.status || '未知'})`);
  347. reject(error);
  348. }
  349. });
  350. });
  351. } catch (e) {
  352. console.error("W3C校验请求失败:", e);
  353. alert(`W3C校验请求失败:${e.message},请检查控制台日志!`);
  354. }
  355. }
  356.  
  357. async function autoRunCssValidation() {
  358. const rawCss = await fetchAndFormatCss();
  359. if (rawCss) {
  360. const formattedCss = formatCssWithJsBeautify(rawCss);
  361. if (formattedCss) {
  362. validateCss(rawCss, formattedCss, true);
  363. }
  364. }
  365. }
  366.  
  367. async function checkCssFileWithW3C() {
  368. const cssFileUrl = getCssFileUrl();
  369. try {
  370. const response = await fetch(cssFileUrl, {
  371. method: 'GET',
  372. cache: 'no-store'
  373. });
  374. if (!response.ok) {
  375. alert(`无法加载CSS文件: ${cssFileUrl} (状态码: ${response.status})`);
  376. return;
  377. }
  378.  
  379. const cssText = await response.text();
  380. if (!cssText.trim()) {
  381. alert("CSS文件为空!");
  382. return;
  383. }
  384.  
  385. console.log("要校验的CSS内容:", cssText);
  386. await validateCssWithW3C(cssText);
  387. } catch (err) {
  388. console.error("获取CSS文件失败:", err);
  389. alert(`获取CSS文件失败:${err.message},请检查控制台日志!`);
  390. }
  391. }
  392.  
  393. function initializeScript() {
  394. const isAutoRunEnabled = GM_getValue("autoRun", true);
  395.  
  396. GM_registerMenuCommand(isAutoRunEnabled ? "关闭自动运行" : "开启自动运行", () => {
  397. GM_setValue("autoRun", !isAutoRunEnabled);
  398. alert(`自动运行已${isAutoRunEnabled ? "关闭" : "开启"}!`);
  399. });
  400.  
  401. GM_registerMenuCommand("验证CSS文件(本地)", async () => {
  402. const rawCss = await fetchAndFormatCss();
  403. if (rawCss) {
  404. const formattedCss = formatCssWithJsBeautify(rawCss);
  405. if (formattedCss) {
  406. validateCss(rawCss, formattedCss, false);
  407. }
  408. }
  409. });
  410.  
  411. GM_registerMenuCommand("验证CSS文件(W3C)", () => {
  412. checkCssFileWithW3C();
  413. });
  414.  
  415. if (isAutoRunEnabled) {
  416. autoRunCssValidation();
  417. }
  418. }
  419.  
  420. initializeScript();
  421. })();