[
  {
    "path": ".editorconfig",
    "content": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintignore",
    "content": "**/lib/*\nnode_modules\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  env: {\n    browser: true,\n    node: true,\n  },\n  parserOptions: {\n    parser: 'babel-eslint',\n  },\n  extends: [\n    '@nuxtjs',\n    'prettier',\n    'prettier/vue',\n    'plugin:prettier/recommended',\n    'plugin:nuxt/recommended',\n    'plugin:json/recommended',\n  ],\n  plugins: ['prettier'],\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "build/*\nmanifest.json\nnode_modules\npackage-lock.json\n\n!.gitkeep\n\nThumbs.db\nDesktop.ini\n*.DS_Store\n*.log\n._*\n.idea\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"semi\": false,\n  \"arrowParens\": \"always\",\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Elbert Alias\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\n# Demodal\n\nDemodal is a browser extension that automatically removes content blocking modals including paywalls, \ndiscount offers, promts to sign up or enter your email address and more.\n\nModal dialogues such as paywalls, discount offers, cookie prompts and GDPR \nbanners are user-hostile interfaces that demand your attention and interrupt\nyour browsing experience. Demodal puts you back in control, letting you\nfocus on the content.\n\n# Installation\n\n### Google Chrome\n\nhttps://chrome.google.com/webstore/detail/demodal/fjhbdkfknppikobblnjibmkmogjeffcf\n\nInstall locally:\n\n* Clone this repository\n* Run `./run manifest v3` or copy `manifest-v3.json` to `manifest.json`\n* Go to `about:extensions`\n* Enable 'Developer mode'\n* Click 'Load unpacked'\n* Select the project folder\n\n### Mozilla Firefox\n\nhttps://addons.mozilla.org/firefox/addon/demodal\n\nInstall locally:\n\n* Clone this repository\n* Run `./run manifest v2` or copy `manifest-v2.json` to `manifest.json`\n* Go go `about:debugging#/runtime/this-firefox`\n* Click 'Load Temporary Add-on'\n* Select `manifest.json`\n\n\n# Contributing\n\n**This extension is in early development. You can help by reporting\nwebsites with modals that didn't get blocked, or by creating your own\ndefinitions and sharing them with us and the community. The aim is to build up\na comprehensive set of rules over time to block modals anywhere.**\n\nDemodal is not an ad-blocker. Only create definitions for UI elements that intefere with reading of content.\n\nWhen submitting a pull request, please include a screenshot of the element that's being blocked and a link to a website to test.\n\nEvery definition should clearly match one modal type (e.g. consent request or paywall). Don't create overly broad definitions (e.g. `div.modal`) that could block legimitate modals.\n\n\n# Specification\n\n## Modal types\n\n| Type       | Description  |\n|------------|--------------|\n| `consent`  | Cookie and GDPR notices.\n| `donate`   | Prompts to make a donation.\n| `email`    | Prompts to enter your email address.\n| `message`  | General messages and notifications.\n| `offer`    | Promotions and discounts.\n| `paywall`  | Prompts to sign up for a paid subscription.\n| `signup`   | Prompts to create an account.\n\n## Definitions\n\nDefinitions are located in [`/definitions`](/definitions), file-separated by modal type. Definitions are grouped by URL pattern.\n\n```javascript\n{\n  \"<glob> [ <glob> ... ]\": { // URL pattern\n    // Definition\n    \"if <function> [ <function> ... ]\": { // Condition\n      \"<selector>\": \"<function> [ <argument> ... ]\" // Action\n    },\n    // Definition (shorthand, no condition)\n    \"<selector>\": \"<function> [ <argument> ... ]\" // Action\n  }\n}\n```\n\n**Examples**\n```javascript\n{\n  \"*.example.com *.example.org\": {\n    \"if $(.modal)\": {\n      \".modal\": \"remove\" // Remove element if present\n    },\n    \".modal\": \"addClass hide\" // Remove element (shorthand)\n    \"if defined(ModalDialog)\": {\n      \"ModalDialog.close\": \"call\" // Call function if defined\n    },\n    \"if defined(ModalDialog)\": {\n      \"ModalDialog.setClosed\": \"call true\" // Call function with arguments\n    },      \n  }\n}\n```\n\n## URL pattern\nURL patterns are defined as [globs](https://en.wikipedia.org/wiki/Glob_(programming)), allowing wildcards (`*`).\n\n| Glob                      | Matches |\n|---------------------------|---------|\n| `*`                       | Any URL.\n| `*.example.com`           | Apex domain and any subdomain, e.g. `example.com`, `www.example.com`.\n| `example.com example.org` | `example.com` and `example.org`.\n| `*.example.com/*/about`   | E.g. `www.example.com/en/about`.\n\n## Conditions\n\nConditions start with `if`, followed by one or more functions. If all functions evaluate to `true`, the specified actions are run.\n\n### Functions\n\n| Function    | Argument             | Description |\n|-------------|----------------------|-------------|\n| `$()`       | [Query selector](https://developer.mozilla.org/docs/Web/API/Document/querySelector) | Tests if an HTML element exists.\n| `defined()` | JavaScript property  | Tests if a JavaScript property exists.\n| `sleep()`   | Time in milliseconds | Returns true after the specified time has passed\n\n## Actions\n\nActions are run when the condition is met, or if no condition is specified.\n\n### Functions\n\n| Function       | Argument   | Description |\n|----------------|------------|-------------|\n| `remove`       |            | Remove the HTML element.\n| `removeParent` | Number     | Remove parent node.\n| `removeIf`     | String     | Remove the HTML element if its text content matches a string.\n| `addClass`     | Class name | Add a class.\n| `removeClass`  | Class name | Remove a class.\n| `addStyle`     | Styles     | Appends CSS to the style attribute.\n| `removeStyle`  |            | Clears the style attribute.\n| `click`        |            | Click the HTML element.\n| `call`         |            | Call the function. Any arguments will be passed to the function.\n\n"
  },
  {
    "path": "_locales/en/messages.json",
    "content": "{\n  \"modalType\": { \"message\": \"Type\" },\n  \"blockedModals\": { \"message\": \"Blocked\" },\n  \"blockedModalsPage\": { \"message\": \"This page\" },\n  \"blockedModalsTotal\": { \"message\": \"All time\" },\n  \"modalTypes\": { \"message\": \"Modal types\" },\n  \"modalTypeOffer\": { \"message\": \"Offers\" },\n  \"modalTypeOfferHelp\": { \"message\": \"Promotions and discounts.\" },\n  \"modalTypePaywall\": { \"message\": \"Paywalls\" },\n  \"modalTypePaywallHelp\": { \"message\": \"Prompts to sign up for a paid subscription.\" },\n  \"modalTypeConsent\": { \"message\": \"Consent requests\" },\n  \"modalTypeConsentHelp\": { \"message\": \"Cookie and GDPR notices.\" },\n  \"modalTypeSignup\": { \"message\": \"Sign-up prompts\" },\n  \"modalTypeSignupHelp\": { \"message\": \"Prompts to create an account.\" },\n  \"modalTypeEmail\": { \"message\": \"Email prompts\" },\n  \"modalTypeEmailHelp\": { \"message\": \"Prompts to enter your email address.\" },\n  \"modalTypeDonate\": { \"message\": \"Donation requests\" },\n  \"modalTypeDonateHelp\": { \"message\": \"Prompts to make a donation.\" },\n  \"modalTypeMessage\": { \"message\": \"Messages\" },\n  \"modalTypeMessageHelp\": { \"message\": \"General messages and notifications.\" },\n  \"modalStatsEmpty\": { \"message\": \"No modals blocked.\" },\n  \"allowOnWebsite\": { \"message\": \"Allow modals on this website\" },\n  \"options\": { \"message\": \"Options\" },\n  \"optionModalTypesHelp\": { \"message\": \"Choose the modal types you wish to block.\" },\n  \"tabBlockedModals\": { \"message\": \"Blocked modals\" },\n  \"blockedModalsHelp\": { \"message\": \"This extension is in early development. If a modal did not get blocked, please report it or create a new definition and share it.\" },\n  \"tabDefinitions\": { \"message\": \"Definitions\" },\n  \"definitionsHelp\": { \"message\": \"Did a modal not get blocked? Create your own definition here and share it.\" },\n  \"readMore\": { \"message\": \"Read more\" }\n}\n"
  },
  {
    "path": "_locales/ru/messages.json",
    "content": "{\n  \"modalType\": { \"message\": \"Тип\" },\n  \"blockedModals\": { \"message\": \"Заблокировано\" },\n  \"blockedModalsPage\": { \"message\": \"На этой странице\" },\n  \"blockedModalsTotal\": { \"message\": \"За всё время\" },\n  \"modalTypes\": { \"message\": \"Типы всплывающих сообщений\" },\n  \"modalTypeOffer\": { \"message\": \"Акции и скидки\" },\n  \"modalTypeOfferHelp\": { \"message\": \"Предложения купить что либо.\" },\n  \"modalTypePaywall\": { \"message\": \"Платный доступ\" },\n  \"modalTypePaywallHelp\": { \"message\": \"Предложения оформить платную подписку.\" },\n  \"modalTypeConsent\": { \"message\": \"Уведомление о cookie\" },\n  \"modalTypeConsentHelp\": { \"message\": \"Уведомления о cookie и GDPR.\" },\n  \"modalTypeSignup\": { \"message\": \"Предложения зарегистрироваться\" },\n  \"modalTypeSignupHelp\": { \"message\": \"Навязчивое предложение создать аккаунт.\" },\n  \"modalTypeEmail\": { \"message\": \"Email рассылки\" },\n  \"modalTypeEmailHelp\": { \"message\": \"Навязчивое предложение указать адрес электронной почты.\" },\n  \"modalTypeDonate\": { \"message\": \"Просьба о пожертвовании\" },\n  \"modalTypeDonateHelp\": { \"message\": \"Предложения сделать пожертвование (задонатить).\" },\n  \"modalTypeMessage\": { \"message\": \"Уведомления\" },\n  \"modalTypeMessageHelp\": { \"message\": \"Назойливые сообщения и уведомления.\" },\n  \"modalStatsEmpty\": { \"message\": \"Ни одно всплывающие сообщение не заблокировано.\" },\n  \"allowOnWebsite\": { \"message\": \"Разрешить всплывающие сообщения на этом сайте\" },\n  \"options\": { \"message\": \"Настройки\" },\n  \"optionModalTypesHelp\": { \"message\": \"Выберите, какие всплывающие сообщения вы хотите блокировать.\" },\n  \"tabBlockedModals\": { \"message\": \"Заблокированные всплывающие сообщения\" },\n  \"blockedModalsHelp\": { \"message\": \"Это расширение находится на ранней стадии разработки. Если всплывающие сообщение не был заблокирован, пожалуйста, сообщите об этом или создайте новое правило для его блокировки и поделитесь им.\" },\n  \"tabDefinitions\": { \"message\": \"Правила блокировки\" },\n  \"definitionsHelp\": { \"message\": \"Всплывающие сообщение не было заблокировано? Создайте свое правило блокировки здесь и поделитесь им.\" },\n  \"readMore\": { \"message\": \"Читать подробнее\" }\n}\n"
  },
  {
    "path": "background/background.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n\n\t\t<title></title>\n  </head>\n  <body>\n    <script src=\"../common/common.js\"></script>\n\t\t<script src=\"background.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "background/background.js",
    "content": "/* globals chrome, importScripts, Common */\n\nif (typeof importScripts !== 'undefined') {\n  importScripts(chrome.runtime.getURL(`common/common.js`))\n}\n\nconst { modalTypes, transformDefinitions } = Common\n\nconst definitions = []\n\nasync function loadDefinitions() {\n  try {\n    definitions.push(\n      ...(await transformDefinitions(\n        await Promise.all(\n          modalTypes.map(async (type) => ({\n            type,\n            definitions: JSON.parse(\n              await (\n                await fetch(chrome.runtime.getURL(`definitions/${type}.json`))\n              ).text()\n            ),\n          }))\n        )\n      ))\n    )\n  } catch (error) {\n    Background.error(error)\n  }\n}\n\nconst Background = {\n  async getDefinitions() {\n    const url = this.sender.url\n\n    // Get custom definitions\n    const { customDefinitions: definitionsByType } =\n      await chrome.storage.sync.get({\n        customDefinitions: {},\n      })\n\n    const customDefinitions = await transformDefinitions(\n      modalTypes.map((type) => ({\n        type,\n        definitions: definitionsByType[type] || {},\n      }))\n    )\n\n    return [...definitions, ...customDefinitions]\n      .filter(({ regExps }) =>\n        regExps.some((_regExp) => {\n          const regExp = new RegExp(_regExp, 'i')\n\n          return (\n            regExp.test(url) ||\n            regExp.test(`${url.replace(/^http(s)?:\\/\\//, 'http$1://www.')}`)\n          )\n        })\n      )\n      .map(({ definitions }) => definitions)\n      .flat()\n  },\n\n  setBadge(text) {\n    return chrome.action.setBadgeText({\n      text: String(text),\n      tabId: this.sender.tab.id,\n    })\n  },\n\n  log(...args) {\n    // eslint-disable-next-line no-console\n    console.log(...args)\n  },\n\n  error(error) {\n    // eslint-disable-next-line no-console\n    console.error(error)\n  },\n}\n\nchrome.runtime.onMessage.addListener((request, sender, sendResponse) => {\n  const { func } = request\n\n  if (func) {\n    const args = request.args || []\n\n    if (request.func === 'log') {\n      // eslint-disable-next-line no-console\n      console.log('content:', ...args)\n    } else if (request.func === 'error') {\n      const [message, stack] = args\n\n      const error = new Error(`content: ${message}`)\n\n      error.stack = stack\n\n      Background.error(error)\n    } else {\n      Promise.resolve(Background[func].call({ request, sender }, ...args))\n        .then((response) => {\n          // eslint-disable-next-line no-console\n          console.log(`content: ${func}(${args.join(', ')})`, response)\n\n          sendResponse(response)\n        })\n        .catch((error) => Background.error(error))\n    }\n  }\n\n  return true\n})\n\nchrome.action.setBadgeBackgroundColor({\n  color: '#4755b3',\n})\n\nloadDefinitions().then(() =>\n  // eslint-disable-next-line no-console\n  console.log(`init ok: ${definitions.length} definitions`)\n)\n"
  },
  {
    "path": "common/common.css",
    "content": ":root {\n  --color-primary: #667aff;\n  --color-primary-dark: #4755b3;\n  --color-primary-light: #f1f3ff;\n  --color-heading: #333;\n  --color-text: #4a4a4a;\n  --color-grey: #ccc;\n  --color-grey-light: #f4f4f4;\n  --color-white: #fff;\n  --radius: 5px;\n}\n\n*, *::before, *::after {\n  box-sizing: border-box;\n}\n\n* {\n  margin: 0;\n}\n\nbody {\n  background-color: var(--color-white);\n  color: var(--color-text);\n  direction: __MSG_@@bidi_dir__;\n\tfont-family: Helvetica, Arial, sans-serif;\n  font-size: .9rem;\n  line-height: 1.5rem;\n  -webkit-font-smoothing: antialiased;\n}\n\np {\n  margin-bottom: 1rem;\n}\n\na {\n  color: var(--color-primary);\n}\n\ninput, button, textarea, select {\n  background-color: var(--color-white);\n  font: inherit;\n}\n\ninput[type=\"text\"], textarea, select {\n  display: block;\n  padding: .5rem;\n  border: 1px solid var(--color-grey);\n  border-radius: var(--radius);\n  margin-bottom: 1rem;\n  width: 100%;\n}\n\ninput[type=\"checkbox\"], input[type=\"radio\"] {\n  margin-right: .5rem;\n}\n\ntextarea.code {\n  font-family: Monaco, monospace;\n  font-size: .7rem;\n  line-height: 1.1rem;\n}\n\nh2 {\n  color: var(--color-heading);\n  font-size: .9rem;\n  margin-bottom: .5rem;\n}\n\ntable {\n  border-collapse: collapse;\n  border-style: hidden;\n  border-spacing: 0;\n  font-size: .8rem;\n  width: 100%;\n}\n\nth {\n  color: var(--color-heading);\n  text-align: left;\n}\n\nth, td {\n  padding: .25rem .5rem;\n}\n\nth:first-child, td:first-child {\n  border-radius: var(--radius) 0 0 var(--radius);\n}\n\nth:last-child, td:last-child {\n  border-radius: 0 var(--radius) var(--radius) 0;\n}\n\ntr:first-child th,\ntr:nth-child(even) td {\n  background: var(--color-primary-light);\n}\n\nhr {\n  border-color: var(--color-grey);\n  border-style: solid;\n  border-width: 1px 0 0 0;\n}\n\n.row {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.label {\n  font-weight: bold;\n}\n\n.checkbox label {\n  display: flex;\n  align-items: center;\n  margin-bottom: .5rem;\n}\n\n.radio label {\n  display: inline-flex;\n  align-items: center;\n  margin-right: .5rem;\n}\n\n.help {\n  font-size: .8rem;\n  margin-bottom: .5rem;\n}\n\n.input-error {\n  color: #ff5353;\n  font-size: .8rem;\n  margin: -.5rem 0 .5rem 0;\n}\n\n.input-error:empty {\n  margin: 0;\n}\n\n.hidden {\n  display: none;\n}\n\n.icon {\n  color: #fff;\n  height: 1.1rem;\n  vertical-align: middle;\n  width: 1.1rem;\n}\n\n.container {\n  margin: 1rem;\n}\n\n.tabs {\n  border-bottom: 1px solid var(--color-grey);\n  display: flex;\n}\n\n.tab {\n  border-bottom: 2px solid transparent;\n  color: var(--color-primary);\n  cursor: pointer;\n  padding: .8rem 1rem;\n  margin-bottom: -1px;\n}\n\n.tab:hover {\n  border-bottom-color: var(--color-primary);\n}\n\n.tab--active {\n  border-bottom-color: var(--color-primary);\n}\n\n.alert {\n  background-color: #ffffcc;\n  color: #999900;\n  font-size: .8rem;\n  padding: 1rem;\n}\n\n.alert a, .alert a:hover, .alert a:active, .alert a:visited {\n  color: #999900;\n}\n"
  },
  {
    "path": "common/common.js",
    "content": "/* globals chrome */\n\n// Manifest v2 polyfill\nif (chrome.runtime.getManifest().manifest_version === 2) {\n  chrome.action = chrome.browserAction\n\n  chrome.storage.sync = {\n    get: (...args) =>\n      new Promise((resolve) => chrome.storage.local.get(...args, resolve)),\n    set: (...args) =>\n      new Promise((resolve) => chrome.storage.local.set(...args, resolve)),\n  }\n}\n\nconst Common = {\n  modalTypes: [\n    'offer',\n    'paywall',\n    'email',\n    'signup',\n    'consent',\n    'donate',\n    'message',\n  ],\n\n  $:\n    typeof window !== 'undefined'\n      ? document.querySelector.bind(document)\n      : () => {},\n  $$:\n    typeof window !== 'undefined'\n      ? document.querySelectorAll.bind(document)\n      : () => {},\n\n  isObject(object) {\n    return typeof object === 'object' && object && !Array.isArray(object)\n  },\n\n  arrayify(item) {\n    return Array.isArray(item) ? item : [item]\n  },\n\n  debounce(func, wait) {\n    let timeout\n\n    return (...args) => {\n      const debounced = () => {\n        clearTimeout(timeout)\n\n        func(...args)\n      }\n\n      clearTimeout(timeout)\n\n      timeout = setTimeout(debounced, wait)\n    }\n  },\n\n  i18n() {\n    const elements = Common.$$('[data-i18n]')\n\n    elements.forEach((element) => {\n      element.textContent = chrome.i18n.getMessage(element.dataset.i18n)\n    })\n  },\n\n  capitalize(string) {\n    return string.charAt(0).toUpperCase() + string.slice(1)\n  },\n\n  removeChildren(element) {\n    while (element.firstChild) {\n      element.removeChild(element.firstChild)\n    }\n  },\n\n  el(name) {\n    return document.createElement(name)\n  },\n\n  getActiveTab() {\n    return new Promise((resolve) => {\n      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) =>\n        resolve(tabs[0])\n      )\n    })\n  },\n\n  async tokenify(string) {\n    const tokens = []\n\n    let bracketLevel = 0\n    let quote = false\n    let token = ''\n\n    for (const char of string.split('')) {\n      token += char\n\n      if (token === 'if ') {\n        token = ''\n      }\n\n      switch (char) {\n        case \"'\":\n          if (!bracketLevel) {\n            quote = !quote\n\n            if (!quote) {\n              tokens.push(token.replace(/(^'|'$)/g, ''))\n\n              token = ''\n            }\n          }\n\n          break\n        case '(':\n          if (!quote) {\n            bracketLevel++\n          }\n\n          break\n        case ')':\n          if (!quote) {\n            bracketLevel--\n\n            if (!bracketLevel) {\n              const [, func, arg] = token.trim().match(/([^(]+)\\((.+)\\)$/)\n\n              if (!Common.Functions[func]) {\n                throw new Error(`Function does not exist: ${func}`)\n              }\n\n              try {\n                await Common.Functions[func].call({ validateOnly: true }, arg)\n              } catch (error) {\n                throw new Error(`Invalid argument: ${arg}`)\n              }\n\n              tokens.push({ func, arg })\n\n              token = ''\n            }\n          }\n\n          break\n      }\n    }\n\n    if (token) {\n      tokens.push(token)\n    }\n\n    return tokens\n  },\n\n  async transformDefinitions(definitionsByType) {\n    const definitionsByGlob = []\n\n    for (const { type, definitions } of definitionsByType) {\n      if (!Common.isObject(definitions)) {\n        throw new TypeError(\n          `Unexpected definitions type, expected object: ${definitions}`\n        )\n      }\n\n      for (const glob of Object.keys(definitions)) {\n        definitionsByGlob[glob] = definitionsByGlob[glob] || {\n          glob,\n          regExps: Common.globToRegExp(glob),\n          definitions: [],\n        }\n\n        definitionsByGlob[glob].definitions.push(\n          ...(\n            await Promise.all(\n              Common.arrayify(definitions[glob]).map((definition) => {\n                try {\n                  if (!Common.isObject(definition)) {\n                    throw new TypeError(\n                      `Unexpected definition type, expected object: ${definition}`\n                    )\n                  }\n\n                  return Promise.all(\n                    Object.keys(definition).map(async (key) => {\n                      const conditions = key.startsWith('if ')\n                        ? await Common.tokenify(key)\n                        : null\n\n                      if (conditions && !conditions.length) {\n                        throw new Error(`Invalid condition: ${key}`)\n                      }\n\n                      let actions = []\n\n                      if (conditions) {\n                        if (!Common.isObject(definition[key])) {\n                          throw new TypeError(\n                            `Invalid actions type, expected object: ${definition[key]}`\n                          )\n                        }\n\n                        actions = Object.keys(definition[key]).map(\n                          (selector) => ({\n                            selector,\n                            action: definition[key][selector],\n                          })\n                        )\n                      } else {\n                        actions = [{ selector: key, action: definition[key] }]\n                      }\n\n                      actions = await Promise.all(\n                        actions.map(async ({ selector, action }) => {\n                          if (typeof action !== 'string') {\n                            throw new TypeError(\n                              `Unexpected action type, expected string`\n                            )\n                          }\n\n                          const [func, ...splitArgs] = action.split(' ')\n\n                          const args = await Common.tokenify(\n                            splitArgs.join(' ')\n                          )\n\n                          return { selector, func, args }\n                        })\n                      )\n\n                      actions.forEach(({ selector, func, args }) => {\n                        try {\n                          Common.$(selector)\n                        } catch (error) {\n                          throw new Error(\n                            `Invalid action selector: ${selector}`\n                          )\n                        }\n\n                        if (!Common.Actions[func]) {\n                          throw new Error(\n                            `Invalid action function: ${func} (${selector})`\n                          )\n                        }\n                      })\n\n                      return { type, conditions: conditions || [], actions }\n                    })\n                  )\n                } catch (error) {\n                  throw new Error(`${error.message || error} in ${glob}`)\n                }\n              })\n            )\n          ).flat()\n        )\n      }\n    }\n\n    return Object.values(definitionsByGlob).flat()\n  },\n\n  globToRegExp(glob) {\n    const globs = glob.split(' ')\n\n    return globs.map((glob) => {\n      try {\n        if (glob !== '*') {\n          if (!glob.includes('.')) {\n            throw new Error('Invalid glob')\n          }\n\n          // eslint-disable-next-line no-new\n          new URL(`https://${glob.replace('*', 'test')}`)\n        }\n\n        return new RegExp(\n          glob === '*'\n            ? '^https?://.+'\n            : `^https?://${glob.replace('.', '\\\\.').replace('*', '[^./]+')}\\\\b`\n        ).source\n      } catch (error) {\n        throw new Error(`Invalid URL pattern: ${glob}`)\n      }\n    })\n  },\n\n  // Run a script in the context of the page\n  inject(func, ...args) {\n    return new Promise((resolve) => {\n      const uid = Math.floor(Math.random() * 900000) + 100000\n\n      const script = Common.el('script')\n\n      script.src = chrome.runtime.getURL('inject/inject.js')\n\n      script.dataset.demodal = 'true'\n\n      script.onload = () => {\n        const receiveMessage = ({ data: { demodalResponse } }) => {\n          if (!demodalResponse || demodalResponse.uid !== uid) {\n            return\n          }\n\n          window.removeEventListener('message', receiveMessage)\n\n          script.remove()\n\n          resolve(demodalResponse.message)\n        }\n\n        window.addEventListener('message', receiveMessage)\n\n        window.postMessage({ demodalRequest: { func, args, uid } })\n      }\n\n      document.body.append(script)\n    })\n  },\n\n  Actions: {\n    remove() {\n      this.remove()\n\n      return true\n    },\n    removeParent(level = 1) {\n      let node = this\n\n      for (let index = 0; index < level; index++) {\n        node = node ? node.parentNode : null\n      }\n\n      if (node) {\n        node.remove()\n\n        return true\n      } else {\n        return false\n      }\n    },\n    removeIf(...args) {\n      if (\n        args.every((string) =>\n          this.textContent.toLowerCase().includes(string.toLowerCase().trim())\n        )\n      ) {\n        this.remove()\n\n        return true\n      }\n\n      return false\n    },\n    addClass(...args) {\n      this.classList.add(...args)\n\n      return true\n    },\n    removeClass(...args) {\n      if (args[0] === '*') {\n        this.className = ''\n      } else {\n        this.classList.remove(...args)\n      }\n\n      return true\n    },\n    addStyle(...args) {\n      this.style = `${this.style}; ${args[0]}`\n\n      return true\n    },\n    removeStyle() {\n      this.style = ''\n\n      return true\n    },\n    click() {\n      this.click()\n\n      return true\n    },\n    call(...args) {\n      Common.inject('call', ...args)\n\n      return true\n    },\n  },\n\n  Functions: {\n    $(selector) {\n      try {\n        return !!Common.$(selector)\n      } catch (error) {\n        throw new Error(`Invalid selector: ${selector}`)\n      }\n    },\n    async defined(...args) {\n      if (this.validateOnly) {\n        return true\n      }\n\n      return await Common.inject('defined', ...args)\n    },\n    sleep(ms = 0) {\n      if (this.validateOnly) {\n        return true\n      }\n\n      return new Promise((resolve) =>\n        setTimeout(() => resolve(true), parseInt(ms, 10))\n      )\n    },\n  },\n\n  Background: {\n    call(func, ...args) {\n      return new Promise((resolve, reject) =>\n        chrome.runtime.sendMessage({ func, args }, (response) =>\n          chrome.runtime.lastError\n            ? reject(new Error(chrome.runtime.lastError.message))\n            : resolve(response)\n        )\n      )\n    },\n  },\n\n  Content: {\n    call(func, ...args) {\n      return new Promise((resolve, reject) =>\n        Common.getActiveTab().then((tab) =>\n          chrome.tabs.sendMessage(tab.id, { func, args }, (response) =>\n            chrome.runtime.lastError\n              ? reject(new Error(chrome.runtime.lastError.message))\n              : resolve(response)\n          )\n        )\n      )\n    },\n  },\n}\n"
  },
  {
    "path": "content/content.js",
    "content": "/* eslint-env browser */\n/* globals chrome, Common */\n\nconst { $, debounce, Background, Actions, Functions } = Common\n\nconst definitions = []\n\nconst blockedModals = {}\n\n// Functions that can be called from the popup script\nconst Content = {\n  getBlockedModals() {\n    return blockedModals\n  },\n\n  getUrl() {\n    return window.location.href\n  },\n\n  reload() {\n    window.location.reload()\n  },\n\n  async isAllowed(url = window.location.href) {\n    // Check if hostname is in allow list\n    const { allowList } = await chrome.storage.sync.get({\n      allowList: [],\n    })\n\n    let { hostname } = new URL(url)\n\n    hostname = hostname.replace(/^www\\./, '')\n\n    return allowList.includes(hostname)\n  },\n}\n\n// Log messages in the background script console\nfunction log(...messages) {\n  const error = messages[0]\n\n  if (error instanceof Error) {\n    Background.call('error', error.toString(), error.stack)\n  } else {\n    Background.call('log', ...messages)\n  }\n}\n\nconst run = async () => {\n  try {\n    if (await Content.isAllowed()) {\n      return\n    }\n\n    await Promise.all(\n      definitions.map(async (definition) => {\n        const { type, conditions, actions, completed } = definition\n\n        if (\n          completed ||\n          !(\n            await Promise.all(\n              conditions.map(({ func, arg }) => Functions[func](arg))\n            )\n          ).every((result) => result)\n        ) {\n          return\n        }\n\n        let found = false\n\n        actions.forEach(({ selector, func, args }) => {\n          let success = false\n\n          switch (func) {\n            case 'call':\n              success = Actions[func](selector, ...args)\n\n              break\n            default:\n              // eslint-disable-next-line no-case-declarations\n              const node = $(selector)\n\n              if (node) {\n                success = Actions[func].call(node, ...args)\n              }\n          }\n\n          if (success) {\n            log(`action: ${selector}: ${func}(${args.join(', ')})`)\n          }\n\n          found = found || success\n        })\n\n        if (found) {\n          definition.completed = true\n\n          blockedModals[type] = (blockedModals[type] || 0) + 1\n\n          Background.call(\n            'setBadge',\n            Object.values(blockedModals).reduce((sum, value) => sum + value, 0)\n          )\n\n          // Update all-time totals\n          chrome.storage.sync\n            .get({\n              blockedModals: {},\n            })\n            .then(({ blockedModals }) => {\n              blockedModals[type] = (blockedModals[type] || 0) + 1\n\n              chrome.storage.sync.set({ blockedModals })\n            })\n        }\n      })\n    )\n  } catch (error) {\n    log(error)\n  }\n}\n\n// Listen for messages from popup\nchrome.runtime.onMessage.addListener((request, sender, sendResponse) => {\n  const { func } = request\n\n  if (func) {\n    const args = request.args || []\n\n    Promise.resolve(Content[func].call({ request, sender }, ...args))\n      .then((response) => {\n        // eslint-disable-next-line no-console\n        log(`popup: ${func}(${args.join(', ')})`, response)\n\n        sendResponse(response)\n      })\n      .catch((error) => log(error))\n  }\n\n  return true\n})\n\n//\n;(async () => {\n  try {\n    definitions.push(...(await Background.call('getDefinitions')))\n\n    const runDebounced = debounce(() => run(), 500)\n\n    const mutationObserver = new MutationObserver((mutations) => {\n      // Avoid infinite loop if mutation was done by us\n      if (\n        mutations.every((mutation) =>\n          Array.from(mutation.addedNodes).every((node) => node.dataset.demodal)\n        )\n      ) {\n        return\n      }\n\n      runDebounced()\n    })\n\n    mutationObserver.observe(document.body, { subtree: true, childList: true })\n\n    run()\n  } catch (error) {\n    log(error)\n  }\n})()\n"
  },
  {
    "path": "definitions/consent.json",
    "content": "{\n  \"*\": {\n    \"#cookie-law-info-bar\": \"remove\",\n    \"#moove_gdpr_cookie_info_bar\": \"remove\",\n    \"if defined(BorlabsCookie)\": {\n      \"BorlabsCookie.hideCookieBox\": \"call\"\n    },\n    \"#cookie-banner\": \"remove\",\n    \".cookie-banner\": \"remove\",\n    \"#onetrust-consent-sdk\": \"remove\",\n    \"#CybotCookiebotDialogBodyButtonDecline\": \"click\",\n    \".cookie-bar__close\": \"click\",\n    \"[aria-label='cookieconsent']\": \"remove\",\n    \".cookie-warning-modal\": \"remove\",\n    \".cookie-consent\": \"remove\",\n    \"#cookie-notice\": \"remove\",\n    \".cookie-notice-big\": \"remove\",\n    \"#cookie-bar\": \"remove\",\n    \"if $(#didomi-host)\": {\n      \"#didomi-host\": \"remove\",\n      \"body\": \"removeClass didomi-popup-open\"\n    },\n    \"#cookieBanner\": \"remove\",\n    \"#cconsent-bar\": \"remove\",\n    \".CookieConsent\": \"remove\",\n    \"#gdpr-banner\": \"remove\",\n    \"#gdpr-cookie-message\": \"remove\",\n    \".cookie-notice\": \"remove\",\n    \".cc-banner\": \"remove\",\n    \".js-cookie-consent-overlay\": \"remove\",\n    \"#ccc\": \"remove\",\n    \"#iubenda-cs-banner\": \"remove\",\n    \".wppopups-whole\": \"removeIf 'cookies'\",\n    \".gdpr-disclaimer\": \"remove\",\n    \"._gdprDisclaimer\": \"remove\",\n    \".ce-cookie-banner\": \"remove\",\n    \".cookie-msg\": \"remove\",\n    \".cookie-panel\": \"remove\",\n    \".cookie-popup\": \"remove\",\n    \"#cookie-popup-wrapper\": \"remove\",\n    \"[data-component='CookieBanner']\": \"remove\",\n    \".t-consentPrompt\": \"remove\",\n    \"#privacy-consent\": \"remove\",\n    \"if $(#acceptationCMPWall)\": {\n      \"#acceptationCMPWall\": \"remove\",\n      \"body\": \"removeStyle\"\n    },\n    \".cookies_message\": \"remove\",\n    \"#cookieConsent\": \"remove\",\n    \".cookie-policy\": \"remove\",\n    \"#cookieNoticeAlert\": \"remove\"\n  },\n  \"*.stackexchange.com *.superuser.com *.stackoverflow.com *.mathoverflow.net *.serverfault.com *.askubuntu.com *.stackapps.com\": {\n    \".js-consent-banner\": \"remove\"\n  },\n  \"*.deepl.com\": {\n    \"#dl_cookieBanner\": \"remove\"\n  },\n  \"*.lego.com\": {\n    \".cookies-used-notice\": \"remove\"\n  },\n  \"*.redhat.com\": {\n  \t\"#truste-consent-track\": \"remove\",\n  \t\".redhat-cookie-banner\": \"remove\"\n  },\n  \"*.openweathermap.org\": {\n  \t\"#stick-footer-panel\": \"remove\"\n  }\n}\n"
  },
  {
    "path": "definitions/donate.json",
    "content": "{\n  \"*.theguardian.com\": {\n    \".site-message--banner\": \"remove\",\n    \"#bottom-banner\": \"removeIf 'Support the Guardian'\"\n  },\n  \"*.science.org\": {\n    \".alert-donation\": \"remove\"\n  }\n}\n"
  },
  {
    "path": "definitions/email.json",
    "content": "{\n  \"*\": {\n    \"if $(.elementor-popup-modal)\": {\n      \".elementor-popup-modal\": \"remove\",\n      \"body\": \"removeClass dialog-prevent-scroll\"\n    },\n    \"if $(.sgpb-popup-dialog-main-div-wrapper input[type='email'])\": {\n      \".sgpb-popup-dialog-main-div-wrapper\": \"remove\"\n    },\n    \"if $([aria-modal='true'] .klaviyo-close-form)\": {\n      \"[aria-modal='true'] .klaviyo-close-form\": \"click\"\n    },\n    \"#shopify-section-newsletter-popup .modal__close\": \"click\",\n    \".close-newsletter\": \"click\",\n    \".cp-popup-container\": \"removeIf 'subscribe'\",\n    \".js_modal_exit_intent\": \"removeIf 'subscribe'\"\n  },\n  \"*.npr.org\": {\n    \".newsletter-stickybar\": \"remove\"\n  },\n  \"*.redhat.com\": {\n  \t\".subscribe-sidebar\": \"remove\"\n  }\n}\n"
  },
  {
    "path": "definitions/message.json",
    "content": "{\n  \"*\": {\n    \"if $([id^='sp_message_container_'])\": {\n      \"[id^='sp_message_container_']\": \"remove\",\n      \"html\": \"removeClass sp-message-open\"\n    }\n  },\n  \"*.heraldsun.com.au\": {\n    \".DialogBox\": \"remove\"\n  },\n  \"*.simplywall.st\": {\n    \"if $([data-cy-id='careers-upsell'])\": {\n      \"[data-cy-id='careers-upsell']\": \"removeParent\"\n    }\n  }\n}\n\n"
  },
  {
    "path": "definitions/offer.json",
    "content": "{\n  \"*.forbes.com\": {\n    \"[external-event='offer-close-modal']\": \"click\"\n  },\n  \"*.theconversation.com\": {\n    \".promo\": \"remove\"\n  }\n}\n"
  },
  {
    "path": "definitions/paywall.json",
    "content": "{\n  \"*.nytimes.com\": {\n    \"if $(#gateway-content)\": {\n      \"#gateway-content\": \"remove\",\n      \"#app > div > div\": \"removeClass *\",\n      \"#app > div > div > div:last-child\": \"remove\",\n      \"#site-content\": \"removeStyle\"\n    }\n  },\n  \"*.newyorker.com\": {\n    \".paywall-bar\": \"remove\",\n    \".paywall-modal\": \"remove\"\n  },\n  \"*.wired.com\": {\n    \".persistent-bottom\": \"remove\"\n  },\n  \"*.washingtonpost.com\": {\n    \"if $(.paywall-overlay) sleep(1000)\": {\n      \".paywall-overlay\": \"remove\",\n      \"html\": \"removeStyle\",\n      \"body\": \"addStyle 'overflow: inherit; position: inherit;'\"\n    },\n    \".softwall-overlay\": \"remove\",\n    \"[id^='softwall-']\": \"remove\"\n  },\n  \"*.simplywall.st\": {\n    \"if $(#modal-container)\": {\n      \"#modal-container\": \"remove\",\n      \"#root\": \"addStyle filter: none\"\n    }\n  },\n  \"*.theguardian.com\": {\n    \"#bottom-banner\": \"removeIf 'Start free trial'\"\n  },\n  \"*.bloomberg.com\": {\n    \"if $(#fortress-paywall-container-root)\": {\n      \"#fortress-paywall-container-root\": \"remove\",\n      \"body\": \"addStyle overflow: auto\"\n    }\n  },\n  \"*.nikkei.com\": {\n    \".pw-widget--benefit-pop-up .pianoj-ribbon-close\": \"click\"\n  },\n  \"*.telegraph.co.uk\": {\n    \".martech-modal-component-overlay\": \"remove\"\n  },\n  \"*.nationalgeographic.com\": {\n    \"if $(.EmailStickyFooter__Modal)\": {\n      \".EmailStickyFooter__Modal\": \"remove\",\n      \".Scroll--locked\": \"removeClass Scroll--locked\",\n      \"body\": \"removeStyle\"\n    }\n  }\n}\n"
  },
  {
    "path": "definitions/signup.json",
    "content": "{\n  \"*\": {\n    \".wbounce-modal\": \"removeIf 'newsletter'\",\n    \".leadinModal\": \"remove\",\n    \".wppopups-whole\": \"removeIf 'sign up'\",\n    \".exit-intent\": \"removeIf 'sign up'\",\n    \".exit-intent-modal\": \"remove\",\n    \"[id^='smsbump-form']\": \"remove\",\n    \"#ouibounce-modal\": \"remove\"\n  },\n  \"*.smh.com.au *.theage.com.au\": {\n    \"[data-testid='registration-prompt']\": \"remove\",\n    \"#registrationWall\": \"remove\"\n  },\n  \"*.ishka.com.au\": {\n    \"if $(.sign_up_karma_club_popup) defined(CloseClubPoup)\": {\n      \"CloseClubPoup\": \"call\"\n    }\n  },\n  \"*.nytimes.com\": {\n    \".MAG_web_all_Monthly-Sale-dock\": \"remove\"\n  },\n  \"*.theatlantic.com\": {\n    \"#paywall[data-category^='nudge']\": \"remove\"\n  },\n  \"*.bloomberg.com\": {\n    \"#fortress-preblocked-container-root\": \"remove\"\n  },\n  \"*.ieee.org\": {\n    \".lightbox-popup\": \"removeIf 'create an account'\"\n  },\n  \"*.afr.com\": {\n    \"if $([data-testid='SubscriptionPrompt-close'])\": {\n      \"[data-testid='SubscriptionPrompt-close']\": \"click\"\n    }\n  },\n  \"*.pinterest.com *.pinterest.com.au\": {\n    \"if $([data-test-id='giftWrap'])\": {\n      \"[data-test-id='giftWrap']\": \"remove\",\n      \"body\": \"removeStyle\"\n    }\n  },\n  \"*.vic.gov.au\": {\n    \"#subscribe-banner-react\": \"remove\"\n  },\n  \"*.nature.com\": {\n    \".c-site-messages--nature-briefing-email-variant\": \"remove\"\n  },\n  \"*.boredpanda.com\": {\n    \"if $(#subscribe-before-leaving)\": {\n      \"#subscribe-before-leaving\": \"remove\",\n      \"#modal-backdrop\": \"remove\",\n      \"body\": \"removeClass modal-open\"\n    }\n  },\n  \"*.bbc.com\": {\n    \"if $(.tp-modal-open)\": {\n      \"body\": \"removeClass tp-modal-open\",\n      \".tp-modal\": \"remove\",\n      \".tp-backdrop\": \"remove\"\n    }\n  },\n  \"*.ibtimes.com\": {\n    \".grwf2_backdrop\": \"remove\",\n    \".wf2-popover\": \"remove\"\n  },\n  \"vk.com\": {\n  \t\"#page_bottom_banners_root\": \"remove\",\n  \t\"if $(.UnauthActionBox)\": {\n  \t\t\".UnauthActionBox__close\": \"click\"\n  \t}\n  }\n}\n"
  },
  {
    "path": "inject/inject.js",
    "content": "/* eslint-env browser */\n\n;(function () {\n  try {\n    const chainToProp = (chain) => {\n      return chain\n        .split('.')\n        .reduce(\n          (value, method) =>\n            value &&\n            value instanceof Object &&\n            Object.prototype.hasOwnProperty.call(value, method)\n              ? value[method]\n              : undefined,\n          window\n        )\n    }\n\n    const Functions = {\n      defined(chain) {\n        return chainToProp(chain) !== undefined\n      },\n      call(chain, ...args) {\n        chainToProp(chain)?.(...args)\n      },\n    }\n\n    const receiveMessage = ({ data: { demodalRequest } }) => {\n      if (!demodalRequest) {\n        return\n      }\n\n      const { func, args, uid } = demodalRequest\n\n      removeEventListener('message', receiveMessage)\n\n      postMessage({\n        demodalResponse: {\n          uid,\n          message:\n            func && Functions[func] ? Functions[func](...(args || [])) : false,\n        },\n      })\n    }\n\n    addEventListener('message', receiveMessage)\n  } catch (error) {\n    // Fail quietly\n  }\n})()\n"
  },
  {
    "path": "manifest-v2.json",
    "content": "{\n  \"name\": \"Demodal - Block modals and overlays\",\n  \"description\": \"Demodal automatically removes content blocking modals including paywalls, discount offers, promts to sign up or enter your email address and more.\",\n  \"version\": \"1.0.3\",\n  \"manifest_version\": 2,\n  \"default_locale\": \"en\",\n  \"background\": {\n    \"page\": \"background/background.html\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"https://*/*\", \"http://*/*\"],\n      \"js\": [\n        \"common/common.js\",\n        \"content/content.js\"\n      ]\n    }\n  ],\n  \"permissions\": [\"storage\", \"activeTab\"],\n  \"browser_action\": {\n    \"default_popup\": \"popup/popup.html\",\n    \"default_icon\": {\n      \"128\": \"images/icon-128.png\",\n      \"256\": \"images/icon-256.png\"\n    }\n  },\n  \"options_ui\":{\n    \"page\": \"options/options.html\",\n    \"open_in_tab\": true\n  },\n  \"icons\": {\n    \"128\": \"images/icon-128.png\",\n    \"256\": \"images/icon-256.png\"\n  }\n}\n"
  },
  {
    "path": "manifest-v3.json",
    "content": "{\n  \"name\": \"Demodal - Block modals and overlays\",\n  \"description\": \"Demodal automatically removes content blocking modals including paywalls, discount offers, email address prompts and more.\",\n  \"version\": \"1.0.3\",\n  \"manifest_version\": 3,\n  \"default_locale\": \"en\",\n  \"background\": {\n    \"service_worker\": \"background/background.js\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"https://*/*\", \"http://*/*\"],\n      \"js\": [\n        \"common/common.js\",\n        \"content/content.js\"\n      ]\n    }\n  ],\n  \"permissions\": [\"storage\", \"activeTab\", \"scripting\"],\n  \"action\": {\n    \"default_popup\": \"popup/popup.html\",\n    \"default_icon\": {\n      \"128\": \"images/icon-128.png\",\n      \"256\": \"images/icon-256.png\"\n    }\n  },\n  \"options_page\": \"options/options.html\",\n  \"icons\": {\n    \"128\": \"images/icon-128.png\",\n    \"256\": \"images/icon-256.png\"\n  }\n}\n"
  },
  {
    "path": "options/options.css",
    "content": ".container {\n  max-width: 600px;\n  margin-left: auto;\n  margin-right: auto;\n}\n"
  },
  {
    "path": "options/options.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n\n\t\t<title data-i18n=\"options\"></title>\n\n    <link rel=\"stylesheet\" href=\"../common/common.css\">\n    <link rel=\"stylesheet\" href=\"options.css\">\n  </head>\n  <body>\n    <div class=\"container\">\n      <div class=\"card\">\n      <div class=\"card__content\">\n        <h2 data-i18n=\"modalTypes\"></h2>\n\n        <p data-i18n=\"optionModalTypesHelp\"><p>\n\n        <div id=\"modal-types\"></div>\n      </div>\n      </div>\n    </div>\n\n    <script src=\"../common/common.js\"></script>\n\t\t<script src=\"options.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "options/options.js",
    "content": "/* globals chrome, Common */\n\nconst { $, i18n, el, capitalize, modalTypes } = Common\n\ni18n()\n\n//\n;(async () => {\n  const { optionBlockModalTypes } = await chrome.storage.sync.get({\n    optionBlockModalTypes: modalTypes.reduce(\n      (options, type) => ({ ...options, [type]: true }),\n      {}\n    ),\n  })\n\n  modalTypes.forEach((type) => {\n    const div1 = el('div')\n    const label = el('label')\n    const checkbox = el('input')\n    const div2 = el('div')\n\n    div1.className = 'checkbox'\n\n    checkbox.type = 'checkbox'\n\n    checkbox.checked = optionBlockModalTypes[type]\n\n    checkbox.addEventListener('change', () => {\n      optionBlockModalTypes[type] = checkbox.checked\n\n      chrome.storage.sync.set({ optionBlockModalTypes })\n    })\n\n    div2.textContent = chrome.i18n.getMessage(\n      `modalType${capitalize(type)}Help`\n    )\n\n    div2.className = 'help'\n\n    label.append(\n      checkbox,\n      chrome.i18n.getMessage(`modalType${capitalize(type)}`)\n    )\n\n    div1.append(label, div2)\n\n    $('#modal-types').append(div1)\n  })\n})()\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"devDependencies\": {\n    \"@nuxtjs/eslint-config\": \"^3.1.0\",\n    \"@nuxtjs/eslint-module\": \"^2.0.0\",\n    \"babel-eslint\": \"^10.1.0\",\n    \"eslint\": \"^7.13.0\",\n    \"eslint-config-prettier\": \"^6.15.0\",\n    \"eslint-plugin-json\": \"^2.1.2\",\n    \"eslint-plugin-nuxt\": \"^1.0.0\",\n    \"eslint-plugin-prettier\": \"^3.1.4\",\n    \"prettier\": \"^2.1.2\"\n  }\n}\n"
  },
  {
    "path": "popup/popup.css",
    "content": "body {\n  width: 500px;\n}\n\n.header {\n  background-color: var(--color-primary);\n  display: flex;\n  align-items: center;\n  padding: 1rem;\n}\n\n.header__logo {\n  margin-right: .8rem;\n  width: 24px;\n}\n\n.header__title {\n  color: var(--color-white);\n  font-size: .9rem;\n  text-transform: uppercase;\n}\n\n.header__links {\n  text-align: right;\n  flex: 1 0;\n}\n\n.content {\n  opacity: .5;\n}\n\n.content.visible {\n  opacity: 1;\n}\n\n#input-definitions {\n  height: 300px;\n}\n\n#input-debug {\n  height: 300px;\n}\n"
  },
  {
    "path": "popup/popup.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n\n    <title></title>\n\n    <link rel=\"stylesheet\" href=\"../common/common.css\">\n    <link rel=\"stylesheet\" href=\"popup.css\">\n  </head>\n  <body>\n    <div class=\"header\">\n      <img class=\"header__logo\" src=\"../images/icon-mono-256.png\" alt=\"\" />\n\n      <h1 class=\"header__title\">Demodal</h1>\n\n      <div class=\"header__links\">\n        <a class=\"link-options\" href=\"#\">\n          <svg class=\"icon\" viewBox=\"0 0 24 24\">\n            <path fill=\"currentColor\" d=\"M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z\" />\n          </svg>\n        </a>\n      </div>\n    </div>\n\n    <div class=\"tabs\">\n      <div class=\"tab tab--active\" data-tab=\"blocked-modals\" data-i18n=\"tabBlockedModals\"></div>\n      <div class=\"tab\" data-tab=\"definitions\" data-i18n=\"tabDefinitions\"></div>\n    </div>\n\n    <div class=\"content\">\n      <div data-tab-content=\"blocked-modals\">\n        <div class=\"alert\">\n          <span data-i18n=\"blockedModalsHelp\"></span>\n          <a href=\"https://github.com/AliasIO/demodal/blob/master/README.md\" target=\"_blank\" data-i18n=\"readMore\"></a>\n        </div>\n\n        <div class=\"if-connected\">\n          <div id=\"options\" class=\"container\">\n            <div class=\"checkbox\">\n              <label>\n                <input id=\"input-allowed\" type=\"checkbox\"> <span data-i18n=\"allowOnWebsite\"></span>\n              </label>\n            </div>\n          </div>\n\n          <hr />\n\n          <div id=\"blocked-page\" class=\"container\">\n            <h2 data-i18n=\"blockedModalsPage\"></h2>\n\n            <div id=\"blocked-page__missing\" class=\"hidden\" data-i18n=\"modalStatsEmpty\"></div>\n\n            <table id=\"blocked-page__stats\">\n              <thead>\n                <tr>\n                  <th width=\"50%\">\n                    <span data-i18n=\"modalType\"></span>\n                  </th>\n                  <th width=\"50%\">\n                    <span data-i18n=\"blockedModals\"></span>\n                  </th>\n                </tr>\n              </thead>\n              <tbody></tbody>\n            </table>\n          </div>\n        </div>\n\n        <div id=\"blocked-total\" class=\"container\">\n          <h2 data-i18n=\"blockedModalsTotal\"></h2>\n\n          <div id=\"blocked-total__missing\" class=\"hidden\" data-i18n=\"modalStatsEmpty\"></div>\n\n          <table id=\"blocked-total__stats\">\n            <thead>\n              <tr>\n                <th width=\"50%\">\n                  <span data-i18n=\"modalType\"></span>\n                </th>\n                <th width=\"50%\">\n                  <span data-i18n=\"blockedModals\"></span>\n                </th>\n              </tr>\n            </thead>\n            <tbody></tbody>\n          </table>\n        </div>\n      </div>\n\n      <div class=\"hidden\" data-tab-content=\"definitions\">\n        <div class=\"container\">\n          <p>\n          <span data-i18n=\"definitionsHelp\"></span>\n          <a href=\"https://github.com/AliasIO/demodal/blob/master/README.md\" target=\"_blank\" data-i18n=\"readMore\"></a>\n          </p>\n\n          <select id=\"input-modal-types\"></select>\n\n          <textarea id=\"input-definitions\" class=\"code\"></textarea>\n\n          <textarea id=\"input-debug\" class=\"code hidden\" readonly></textarea>\n\n          <div id=\"errors-definitions\" class=\"input-error\"></div>\n\n          <div class=\"row\">\n            <div class=\"radio\">\n              <label>\n                <input type=\"radio\" name=\"input-mode\" value=\"edit\" checked /> Edit\n              </label>\n\n              <label>\n                <input type=\"radio\" name=\"input-mode\" value=\"debug\" /> Debug\n              </label>\n            </div>\n\n            <div class=\"text-right\">\n              <a href=\"#\" class=\"reload\">Reload page</a>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <script src=\"../common/common.js\"></script>\n    <script src=\"popup.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "popup/popup.js",
    "content": "/* globals chrome, Common */\n\nconst {\n  $,\n  $$,\n  i18n,\n  capitalize,\n  removeChildren,\n  el,\n  debounce,\n  modalTypes,\n  transformDefinitions,\n  Content,\n} = Common\n\nfunction renderTotals(prefix, totals) {\n  let any = false\n\n  removeChildren($(`${prefix}__stats tbody`))\n\n  modalTypes.forEach((type) => {\n    const tr = el('tr')\n    const td1 = el('td')\n    const td2 = el('td')\n\n    if (totals[type]) {\n      td1.textContent = chrome.i18n.getMessage(`modalType${capitalize(type)}`)\n      td2.textContent = totals[type]\n\n      tr.appendChild(td1)\n      tr.appendChild(td2)\n      $(`${prefix}__stats tbody`).appendChild(tr)\n\n      any = true\n    }\n  })\n\n  if (any) {\n    $(`${prefix}__stats`).classList.remove('hidden')\n    $(`${prefix}__missing`).classList.add('hidden')\n  }\n}\n\ni18n()\n\n//\n;(async () => {\n  $('.content').classList.remove('visible')\n\n  $(`#blocked-page__stats`).classList.add('hidden')\n  $(`#blocked-page__missing`).classList.remove('hidden')\n  $(`#blocked-total__stats`).classList.add('hidden')\n  $(`#blocked-total__missing`).classList.remove('hidden')\n\n  $$('.tab').forEach((tab) =>\n    tab.addEventListener('click', (e) => {\n      $$('.tab').forEach((tab) => tab.classList.remove('tab--active'))\n\n      e.target.classList.add('tab--active')\n\n      $$('[data-tab-content]').forEach((tabContent) =>\n        tabContent.classList.add('hidden')\n      )\n\n      $(`[data-tab-content='${e.target.dataset.tab}']`).classList.remove(\n        'hidden'\n      )\n    })\n  )\n\n  // Blocked modals tab\n\n  $('.link-options').addEventListener('click', (e) => {\n    e.preventDefault()\n\n    chrome.runtime.openOptionsPage()\n  })\n\n  // Show all-time totals\n  const { blockedModals: totals } = await chrome.storage.sync.get({\n    blockedModals: {},\n  })\n\n  renderTotals('#blocked-total', totals)\n\n  let connected = true\n\n  try {\n    // Show page totals\n    const blockedModals = await Content.call('getBlockedModals')\n\n    renderTotals('#blocked-page', blockedModals)\n  } catch (error) {\n    // eslint-disable-next-line no-console\n    console.error(error)\n\n    connected = false\n  }\n\n  // Show page specific content only if we have a content script connection\n  $$('.if-connected').forEach((element) =>\n    element.classList[connected ? 'remove' : 'add']('hidden')\n  )\n\n  if (connected) {\n    // Add/remove hostnames to allow list\n    const allowed = await Content.call('isAllowed')\n\n    $('#input-allowed').checked = allowed\n\n    $('#input-allowed').addEventListener('click', async (el) => {\n      const { allowList } = await chrome.storage.sync.get({\n        allowList: [],\n      })\n\n      const url = await Content.call('getUrl')\n\n      let { hostname } = new URL(url)\n\n      hostname = hostname.replace(/^www\\./, '')\n\n      if (el.target.checked) {\n        allowList.push(hostname)\n      } else {\n        const index = allowList.findIndex((_hostname) => _hostname === hostname)\n\n        if (index !== -1) {\n          allowList.splice(index, 1)\n        }\n      }\n\n      chrome.storage.sync.set({ allowList })\n\n      Content.call('reload')\n    })\n  }\n\n  // Definitions tab\n\n  // Modal type select\n  modalTypes.forEach((type) => {\n    const option = el('option')\n\n    option.value = type\n    option.textContent = chrome.i18n.getMessage(`modalType${capitalize(type)}`)\n\n    $('#input-modal-types').append(option)\n  })\n\n  $('#input-modal-types').addEventListener('change', (event) => {\n    const modalType = event.target.value\n\n    $('#errors-definitions').textContent = ''\n\n    chrome.storage.sync\n      .get({\n        customDefinitions: {},\n      })\n      .then(\n        ({ customDefinitions }) =>\n          ($('#input-definitions').value = JSON.stringify(\n            customDefinitions[modalType] || {},\n            null,\n            2\n          ))\n      )\n  })\n\n  $('#input-modal-types').dispatchEvent(new Event('change'))\n\n  // Edit / debug toggle\n  $$('input[name=\"input-mode\"]').forEach((el) =>\n    el.addEventListener('change', (event) => {\n      const debug = event.target.value === 'debug'\n\n      $('#input-definitions').classList[debug ? 'add' : 'remove']('hidden')\n      $('#input-debug').classList[debug ? 'remove' : 'add']('hidden')\n    })\n  )\n\n  // Reload page\n  $('.reload').addEventListener('click', () => Content.call('reload'))\n\n  // Format JSON on blur\n  $('#input-definitions').addEventListener('blur', (event) => {\n    const json = event.target.value || '{}'\n\n    try {\n      const definitions = JSON.parse(json)\n\n      event.target.value = JSON.stringify(definitions, null, 2)\n\n      const selectedType = $('#input-modal-types').value\n\n      chrome.storage.sync\n        .get({\n          customDefinitions: modalTypes.reduce(\n            (definitions, type) => ({ ...definitions, [type]: {} }),\n            {}\n          ),\n        })\n        .then(({ customDefinitions }) =>\n          chrome.storage.sync.set({\n            customDefinitions: {\n              ...customDefinitions,\n              [selectedType]: definitions,\n            },\n          })\n        )\n    } catch (error) {\n      // eslint-disable-next-line no-console\n      console.error(error)\n    }\n  })\n\n  // Validate JSON on change\n  $('#input-definitions').addEventListener(\n    'input',\n    debounce((event) => {\n      const json = event.target.value || '{}'\n\n      try {\n        const definitionsByType = [\n          {\n            type: 'offers',\n            definitions: JSON.parse(json),\n          },\n        ]\n\n        const definitions = transformDefinitions(definitionsByType)\n\n        $('#input-debug').textContent = JSON.stringify(definitions, null, 2)\n\n        $('#errors-definitions').textContent = ''\n      } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error)\n\n        $('#errors-definitions').textContent = error.message || error.toString()\n      }\n    }, 500)\n  )\n\n  $('.content').classList.add('visible')\n})()\n"
  },
  {
    "path": "run",
    "content": "#!/bin/bash\n\ncase $1 in\n  manifest)\n    if [[ -f manifest-$2.json ]]; then\n      cat manifest-$2.json > manifest.json\n\n      echo Switched to manifest $2\n    else\n      echo Invalid argument $2\n    fi\n    ;;\n\n  build)\n    version=$2\n\n    if [ -z \"$version\" ]; then\n      echo \"No version specified\"\n\n      exit\n    fi\n\n    sed -i '' -r \"s/\\\"version\\\": \\\".+\\\"/\\\"version\\\": \\\"$2\\\"/\" manifest-v2.json\n    sed -i '' -r \"s/\\\"version\\\": \\\".+\\\"/\\\"version\\\": \\\"$2\\\"/\" manifest-v3.json\n\n    find . -name '.DS_Store' -type f -delete\n\n    files=\"_locales background common content definitions images inject options popup manifest.json\"\n\n    cp manifest.json manifest.json.bak\n\n    cat manifest-v2.json > manifest.json\n\n    rm build/*\n\n    zip -r build/demodal-v2.zip $files\n\n    cat manifest-v3.json > manifest.json\n\n    zip -r build/demodal-v3.zip $files\n\n    mv manifest.json.bak manifest.json\n    ;;\n\n  *)\n    echo Invalid argument $1\n    ;;\nesac\n"
  }
]