// ==UserScript== // @name AdBlock Script for WebView // @name:zh-CN 套壳油猴的广告拦截脚本 // @author Lemon399 // @version 2.0.1 // @description Parse ABP Cosmetic rules to CSS and apply it. // @description:zh-CN 将 ABP 元素中的隐藏规则转换为 CSS 使用 // @require https://greasyfork.org/scripts/452263-extended-css/code/extended-css.js?version=1099366 // @match *://*/* // @run-at document-start // @grant GM_getValue // @grant GM_deleteValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_xmlhttpRequest // @grant GM_addStyle // @namespace https://lemon399-bitbucket-io.vercel.app/ // @source https://gitee.com/lemon399/tampermonkey-cli/tree/master/projects/abp_parse // @connect code.gitlink.org.cn // @copyright GPL-3.0 // @license GPL-3.0 // @history 2.0.1 兼容 Tampermonkey 4.18,代码兼容改为 ES6 // @downloadURL none // ==/UserScript== (function (tm, ExtendedCss) { "use strict"; function _interopDefaultLegacy(e) { return e && typeof e === "object" && "default" in e ? e : { default: e, }; } var ExtendedCss__default = _interopDefaultLegacy(ExtendedCss); function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } const onlineRules = [ "https://code.gitlink.org.cn/damengzhu/banad/raw/branch/main/jiekouAD.txt", "https://code.gitlink.org.cn/damengzhu/abpmerge/raw/branch/main/abpmerge.txt", ], defaultRules = ` ! 没有两个 # 的行和 开头为 ! 的行会忽略 ! baidu.com##.ec_wise_ad ! ! :remove() 会用 js 移除元素,:remove() 必须放在行尾 ! baidu.com###ad:remove() ! ! 由于语法限制,内置规则中 ! 一个反斜杠需要改成两个,像这样 \\ ! ! 脚本会首先尝试从上面的地址数组获取规则 ! 获取到的规则将会与内置规则合并 ! 所有规则获取完毕以后才会应用规则 ! ! 若要修改地址,请注意同步修改头部的 @connect 的域名 !2.3.1 vercel.app#?#blockquote:has(.mymoney) vercel.app#?#blockquote:-abp-has(.myhoney) vercel.app#?#blockquote[-ext-has=".mytony"] !2.3.2 vercel.app#?#blockquote:has-text(烦恼) vercel.app#?#blockquote:has-text(/区分\\d/) vercel.app#?#blockquote:contains(滑块) vercel.app#?#blockquote:-abp-contains(红日) vercel.app#?#blockquote[-ext-contains="媒体"] !2.3.3 vercel.app#?#blockquote:matches-css(background-color: rgb\\(135, 206, 235\\)) vercel.app#?#blockquote:matches-css(background-color: rgb\\(200, 206, 214\\)) vercel.app#?#blockquote[-ext-matches-css="background-color: rgb\\(240, 255, 240\\)"] vercel.app#?#blockquote:matches-css(background-color: /^rgb\\(255,/) !2.3.4 vercel.app#?#blockquote:matches-css-before(content: 我是广告啊) vercel.app#?#blockquote[-ext-matches-css-before="content: 我是广告呢"] !2.3.5 vercel.app#?#blockquote:matches-css-after(content: 我是广告哟) vercel.app#?#blockquote[-ext-matches-css-after="content: 我是广告哦"] !2.3.6 vercel.app#?#[type=range]:matches-attr("disabled") vercel.app#?#[type=range]:matches-attr("min"="5") vercel.app#?#[type=range]:matches-attr("max"="/^3/") !2.3.9 vercel.app#?#[src$="up.gif"]:nth-ancestor(2) !2.3.10 vercel.app#?#[src$="up2.gif"]:upward(2) vercel.app#?#p > em:upward(.box) !2.3.12 vercel.app#?##close:xpath(../../*[1]) !2.3.13 vercel.app#?##remo:remove() !2.3.15 vercel.app#?##not > blockquote:not(:has(.ok)) vercel.app#?##abpnot > blockquote:not(:-abp-has(.ok)) !2.3.16 vercel.app#?##ifnot > blockquote:if-not(.ok) !2.2.4 vercel.app#?#blockquote:has(.yes) vercel.app#@?#blockquote:has(.yes) !2.2.10 vercel.app#$##turq { color: turquoise !important } !2.2.10@ vercel.app#$##seag { color: seagreen !important } vercel.app#@$##seag { color: seagreen !important } !2.2.11 vercel.app#$?#span:contains(真的是) { display: none!important; } !2.2.11@ vercel.app#$?#span:contains(真不是) { display: none!important; } vercel.app#@$?#span:contains(真不是) { display: none!important; } `; const id = "placeholder"; function isObj(o) { return ( typeof o == "object" && (o === null || o === void 0 ? void 0 : o.toString()) === "[object Object]" ); } function runNeed(condition, fn, option, ...args) { let ok = false, sleep = (time) => { return new Promise((r) => setTimeout(r, time)); }, defaultOption = { count: 20, delay: 200, failFn: () => null, }; if (isObj(option)) Object.assign(defaultOption, option); new Promise(async (resolve, reject) => { for (let c = 0; !ok && c < defaultOption.count; c++) { await sleep(defaultOption.delay); ok = condition.call(null, c + 1); } ok ? resolve() : reject(); }).then(fn.bind(null, ...args), defaultOption.failFn); } `BEXT_LAST_CHECK_KEY_${id}`; function getName(path) { const reer = /\/([^\/]+)$/.exec(path); return reer ? reer[1] : null; } function getEtag(header) { const reer = /etag: \"(\w+)\"/.exec(header); return reer ? reer[1] : null; } function getDay(date) { const reer = /\/(\d{1,2}) /.exec(date); return reer ? parseInt(reer[1]) : 0; } function makeRuleBox() { return { black: [], white: [], apply: "", }; } function domainChecker(domains) { const results = [], hasTLD = /\.+?[\w-]+$/, urlSuffix = hasTLD.exec(location.hostname); let invert = false, result = false, mostMatch = { long: 0, result: undefined, }; domains.forEach((domain) => { if (domain.endsWith(".*") && Array.isArray(urlSuffix)) { domain = domain.replace(".*", urlSuffix[0]); } if (domain.startsWith("~")) { invert = true; domain = domain.slice(1); } else invert = false; result = location.hostname.endsWith(domain); results.push(result !== invert); if (result) { if (domain.length > mostMatch.long) { mostMatch = { long: domain.length, result: result !== invert, }; } } }); return mostMatch.long > 0 ? mostMatch.result : results.includes(true); } function ruleChecker(matches) { const index = matches.findIndex((i) => i !== null); if ( index >= 0 && (!matches[index][1] || domainChecker(matches[index][1].split(","))) ) { return [index % 2 == 0, Math.floor(index / 2), matches[index].pop()]; } } function ruleSpliter(rule) { const result = ruleChecker([ rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?##([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\?#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\?#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\$#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\$#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#\$\?#([^\s^+].*)/ ), rule.match( /^(~?[\w-]+\.([\w-]+|\*)(,~?[\w-]+\.([\w-]+|\*))*)?#@\$\?#([^\s^+].*)/ ), ]); if (result && result[2]) { return { black: result[0], type: result[1], sel: result[2], }; } } const selectors = makeRuleBox(), extSelectors = makeRuleBox(), styles = makeRuleBox(), extStyles = makeRuleBox(), values = { get black() { const v = tm.GM_getValue("ajs_disabled_domains", ""); return typeof v == "string" ? v : ""; }, set black(v) { v === null ? tm.GM_deleteValue("ajs_disabled_domains") : tm.GM_setValue("ajs_disabled_domains", v); }, get rules() { const v = tm.GM_getValue("ajs_saved_abprules", "{}"); return typeof v == "string" ? JSON.parse(v) : {}; }, set rules(v) { v === null ? tm.GM_deleteValue("ajs_saved_abprules") : tm.GM_setValue("ajs_saved_abprules", JSON.stringify(v)); }, get time() { const v = tm.GM_getValue("ajs_rules_ver", "0/0/0 0:0:0"); return typeof v == "string" ? v : "0/0/0 0:0:0"; }, set time(v) { v === null ? tm.GM_deleteValue("ajs_rules_ver") : tm.GM_setValue("ajs_rules_ver", v); }, get etags() { const v = tm.GM_getValue("ajs_rules_etags", "{}"); return typeof v == "string" ? JSON.parse(v) : {}; }, set etags(v) { v === null ? tm.GM_deleteValue("ajs_rules_etags") : tm.GM_setValue("ajs_rules_etags", JSON.stringify(v)); }, }, data = { disabled: false, updating: false, receivedRules: "", allRules: "", genericStyle: document.createElement("style"), presetCss: " {display: none !important;width: 0 !important;height: 0 !important;} ", supportedCount: 0, appliedCount: 0, }, menus = { disable: { id: undefined, get text() { return data.disabled ? "在此网站启用拦截" : "在此网站禁用拦截"; }, }, update: { id: undefined, get text() { const time = values.time; return data.updating ? "正在更新..." : `点击更新: ${time.slice(0, 1) === "0" ? "未知时间" : time}`; }, }, count: { id: undefined, get text() { return `点击清空: ${data.appliedCount} / ${data.supportedCount} / ${ data.allRules.split("\n").length }`; }, }, }; function gmMenu(name, cb) { if ( typeof tm.GM_registerMenuCommand !== "function" || typeof tm.GM_unregisterMenuCommand !== "function" || window.self !== window.top ) return false; const id = menus[name].id; if (typeof id !== "undefined") { tm.GM_unregisterMenuCommand(id); menus[name].id = undefined; } if (typeof cb == "function") { menus[name].id = tm.GM_registerMenuCommand(menus[name].text, cb); } return typeof menus[name].id !== "undefined"; } function promiseXhr(details) { return new Promise((resolve, reject) => { tm.GM_xmlhttpRequest( Object.assign( { onload(e) { resolve(e); }, onabort: reject.bind(null), onerror: reject.bind(null), ontimeout: reject.bind(null), }, details ) ); }); } function storeRule(name, resp) { const savedRules = values.rules, savedEtags = values.etags; if (resp.responseHeaders) { const etag = getEtag(resp.responseHeaders); if (etag) { savedEtags[name] = etag; values.etags = savedEtags; } } if (resp.responseText) { savedRules[name] = resp.responseText; values.rules = savedRules; } } function fetchRule(url) { var _a; const name = (_a = getName(url)) !== null && _a !== void 0 ? _a : `${url.length}.${url.slice(-5)}`; return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { if (!name) reject(); const headResp = yield promiseXhr({ method: "HEAD", responseType: "text", url: url, }); if (headResp.responseText) { storeRule(name, headResp); resolve(); } else { if (headResp.responseHeaders) { const etag = getEtag(headResp.responseHeaders), savedEtags = values.etags; if (etag !== savedEtags[name]) { storeRule( name, yield promiseXhr({ method: "GET", responseType: "text", url: url, }) ); resolve(); } else reject(); } } }) ); } function fetchRules() { return __awaiter(this, void 0, void 0, function* () { const pArray = []; data.updating = true; gmMenu("update", fetchRules); onlineRules.forEach((url) => { pArray.push(fetchRule(url)); }); yield Promise.allSettled(pArray); values.time = new Date().toLocaleString("zh-CN"); initRules(); }); } function performUpdate(force) { if (force) { return fetchRules(); } else { return getDay(values.time) !== new Date().getDate() ? fetchRules() : Promise.resolve(); } } function switchDisabledStat() { const disaList = values.black.split(","), disaResult = disaList.includes(location.hostname); data.disabled = !disaResult; if (data.disabled) { disaList.push(location.hostname); } else { disaList.splice(disaList.indexOf(location.hostname), 1); } values.black = disaList.join(","); gmMenu("disable", switchDisabledStat); } function checkDisableStat() { const disaResult = values.black.split(",").includes(location.hostname); data.disabled = disaResult; gmMenu("disable", switchDisabledStat); return disaResult; } function initRules() { const abpRules = values.rules, abpKeys = Object.keys(abpRules); abpKeys.forEach((name) => { data.receivedRules += "\n" + abpRules[name] + "\n"; }); data.allRules = defaultRules + data.receivedRules; if (abpKeys.length !== 0) { data.updating = false; gmMenu("update", fetchRules); } return data.receivedRules.length; } function styleApply() { const css = styles.apply + (selectors.apply.length > 0 ? selectors.apply + data.presetCss : ""), ecss = extStyles.apply + (extSelectors.apply.length > 0 ? extSelectors.apply + data.presetCss : ""); if (css.length > 0) { if (typeof tm.GM_addStyle == "function") { tm.GM_addStyle(css); } else { runNeed( () => !!document.documentElement, () => { data.genericStyle.textContent = css; document.documentElement.appendChild(data.genericStyle); } ); } } if (ecss.length > 0) new ExtendedCss__default.default({ styleSheet: ecss, }).apply(); } function parseRules() { [selectors, extSelectors].forEach((obj) => { obj.black .filter((v) => !obj.white.includes(v)) .forEach((sel) => { obj.apply += `${obj.apply.length == 0 ? "" : ","}${sel}`; data.appliedCount++; }); }); [styles, extStyles].forEach((obj) => { obj.black .filter((v) => !obj.white.includes(v)) .forEach((sel) => { obj.apply += ` ${sel}`; data.appliedCount++; }); }); gmMenu("count", () => { if (confirm("是否清空存储规则 ?")) { values.rules = {}; values.time = "0/0/0 0:0:0"; values.etags = {}; gmMenu("update", performUpdate.bind(this, true)); } }); styleApply(); } function main() { return __awaiter(this, void 0, void 0, function* () { if (checkDisableStat()) return; if (initRules() === 0) yield performUpdate(true); data.allRules.split("\n").forEach((rule) => { const ruleObj = ruleSpliter(rule); let arr = ""; if (typeof ruleObj !== "undefined") { arr = ruleObj.black ? "black" : "white"; switch (ruleObj.type) { case 0: selectors[arr].push(ruleObj.sel); break; case 1: extSelectors[arr].push(ruleObj.sel); break; case 2: styles[arr].push(ruleObj.sel); break; case 3: extStyles[arr].push(ruleObj.sel); break; } data.supportedCount++; } }); parseRules(); performUpdate(false); }); } main(); })(self, ExtendedCss);