套壳油猴的广告拦截脚本

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

当前为 2022-10-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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