Via Css 检验

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

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