AdBlock Script for WebView

Parse ABP Cosmetic rules to CSS and apply it.

目前為 2022-10-01 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);