套壳油猴的广告拦截脚本

将 ABP 元素中的隐藏规则转换为 CSS 使用

目前为 2022-10-02 提交的版本。查看 最新版本

// ==UserScript==
// @name               AdBlock Script for WebView
// @name:zh-CN         套壳油猴的广告拦截脚本
// @author             Lemon399
// @version            2.0.3
// @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
// @history            2.0.2 修复多个 iframe 首次执行重复下载规则,改进清空功能
// @history            2.0.3 继续改进清空功能
// ==/UserScript==

(function (tm, ExtendedCss) {
  "use strict";

  function _interopDefaultLegacy(e) {
    return e && typeof e === "object" && "default" in e ? e : { default: e };
  }

  var ExtendedCss__default = /*#__PURE__*/ _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 runOnce(fn, key) {
    const uniqId = "BEXT_UNIQ_ID_" + id + (key ? key : "");
    if (uniqId in window) return;
    window[uniqId] = true;
    fn === null || fn === void 0 ? void 0 : fn.call(null);
  }
  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,
      isFrame: window.self !== window.top,
      isClean: false,
      mutex: "__lemon__abp__parser__$__",
    },
    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.isClean
            ? "已清空,点击刷新重新加载规则"
            : `点击清空: ${data.appliedCount} / ${data.supportedCount} / ${
                data.allRules.split("\n").length
              }`;
        },
      },
    };
  function gmMenu(name, cb) {
    if (
      typeof tm.GM_registerMenuCommand !== "function" ||
      typeof tm.GM_unregisterMenuCommand !== "function" ||
      data.isFrame
    )
      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,
        });
        // 不支持 HEAD
        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");
      gmMenu("count", cleanRules);
      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 cleanRules() {
    if (confirm("是否清空存储规则 ?")) {
      values.rules = {};
      values.time = "0/0/0 0:0:0";
      values.etags = {};
      data.appliedCount = 0;
      data.supportedCount = 0;
      data.allRules = "";
      data.isClean = true;
      gmMenu("update");
      gmMenu("count", () => location.reload());
    }
  }
  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", cleanRules);
    styleApply();
  }
  function main() {
    return __awaiter(this, void 0, void 0, function* () {
      if (checkDisableStat() || (initRules() === 0 && data.isFrame)) return;
      if (data.receivedRules.length === 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);
    });
  }
  runOnce(main, data.mutex);
})(
  {
    GM_getValue,
    GM_deleteValue,
    GM_setValue,
    GM_registerMenuCommand,
    GM_unregisterMenuCommand,
    GM_xmlhttpRequest,
    GM_addStyle,
  },
  ExtendedCss
);