Full Code of AliasIO/demodal for AI

master d128b3606914 cached
32 files
53.5 KB
14.9k tokens
39 symbols
1 requests
Download .txt
Repository: AliasIO/demodal
Branch: master
Commit: d128b3606914
Files: 32
Total size: 53.5 KB

Directory structure:
gitextract_jysvujgv/

├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── _locales/
│   ├── en/
│   │   └── messages.json
│   └── ru/
│       └── messages.json
├── background/
│   ├── background.html
│   └── background.js
├── common/
│   ├── common.css
│   └── common.js
├── content/
│   └── content.js
├── definitions/
│   ├── consent.json
│   ├── donate.json
│   ├── email.json
│   ├── message.json
│   ├── offer.json
│   ├── paywall.json
│   └── signup.json
├── inject/
│   └── inject.js
├── manifest-v2.json
├── manifest-v3.json
├── options/
│   ├── options.css
│   ├── options.html
│   └── options.js
├── package.json
├── popup/
│   ├── popup.css
│   ├── popup.html
│   └── popup.js
└── run

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false


================================================
FILE: .eslintignore
================================================
**/lib/*
node_modules


================================================
FILE: .eslintrc.js
================================================
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
  },
  parserOptions: {
    parser: 'babel-eslint',
  },
  extends: [
    '@nuxtjs',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended',
    'plugin:json/recommended',
  ],
  plugins: ['prettier'],
}


================================================
FILE: .gitignore
================================================
build/*
manifest.json
node_modules
package-lock.json

!.gitkeep

Thumbs.db
Desktop.ini
*.DS_Store
*.log
._*
.idea


================================================
FILE: .prettierrc
================================================
{
  "semi": false,
  "arrowParens": "always",
  "singleQuote": true
}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Elbert Alias

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================

# Demodal

Demodal is a browser extension that automatically removes content blocking modals including paywalls, 
discount offers, promts to sign up or enter your email address and more.

Modal dialogues such as paywalls, discount offers, cookie prompts and GDPR 
banners are user-hostile interfaces that demand your attention and interrupt
your browsing experience. Demodal puts you back in control, letting you
focus on the content.

# Installation

### Google Chrome

https://chrome.google.com/webstore/detail/demodal/fjhbdkfknppikobblnjibmkmogjeffcf

Install locally:

* Clone this repository
* Run `./run manifest v3` or copy `manifest-v3.json` to `manifest.json`
* Go to `about:extensions`
* Enable 'Developer mode'
* Click 'Load unpacked'
* Select the project folder

### Mozilla Firefox

https://addons.mozilla.org/firefox/addon/demodal

Install locally:

* Clone this repository
* Run `./run manifest v2` or copy `manifest-v2.json` to `manifest.json`
* Go go `about:debugging#/runtime/this-firefox`
* Click 'Load Temporary Add-on'
* Select `manifest.json`


# Contributing

**This extension is in early development. You can help by reporting
websites with modals that didn't get blocked, or by creating your own
definitions and sharing them with us and the community. The aim is to build up
a comprehensive set of rules over time to block modals anywhere.**

Demodal is not an ad-blocker. Only create definitions for UI elements that intefere with reading of content.

When submitting a pull request, please include a screenshot of the element that's being blocked and a link to a website to test.

Every 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.


# Specification

## Modal types

| Type       | Description  |
|------------|--------------|
| `consent`  | Cookie and GDPR notices.
| `donate`   | Prompts to make a donation.
| `email`    | Prompts to enter your email address.
| `message`  | General messages and notifications.
| `offer`    | Promotions and discounts.
| `paywall`  | Prompts to sign up for a paid subscription.
| `signup`   | Prompts to create an account.

## Definitions

Definitions are located in [`/definitions`](/definitions), file-separated by modal type. Definitions are grouped by URL pattern.

```javascript
{
  "<glob> [ <glob> ... ]": { // URL pattern
    // Definition
    "if <function> [ <function> ... ]": { // Condition
      "<selector>": "<function> [ <argument> ... ]" // Action
    },
    // Definition (shorthand, no condition)
    "<selector>": "<function> [ <argument> ... ]" // Action
  }
}
```

**Examples**
```javascript
{
  "*.example.com *.example.org": {
    "if $(.modal)": {
      ".modal": "remove" // Remove element if present
    },
    ".modal": "addClass hide" // Remove element (shorthand)
    "if defined(ModalDialog)": {
      "ModalDialog.close": "call" // Call function if defined
    },
    "if defined(ModalDialog)": {
      "ModalDialog.setClosed": "call true" // Call function with arguments
    },      
  }
}
```

## URL pattern
URL patterns are defined as [globs](https://en.wikipedia.org/wiki/Glob_(programming)), allowing wildcards (`*`).

| Glob                      | Matches |
|---------------------------|---------|
| `*`                       | Any URL.
| `*.example.com`           | Apex domain and any subdomain, e.g. `example.com`, `www.example.com`.
| `example.com example.org` | `example.com` and `example.org`.
| `*.example.com/*/about`   | E.g. `www.example.com/en/about`.

## Conditions

Conditions start with `if`, followed by one or more functions. If all functions evaluate to `true`, the specified actions are run.

### Functions

| Function    | Argument             | Description |
|-------------|----------------------|-------------|
| `$()`       | [Query selector](https://developer.mozilla.org/docs/Web/API/Document/querySelector) | Tests if an HTML element exists.
| `defined()` | JavaScript property  | Tests if a JavaScript property exists.
| `sleep()`   | Time in milliseconds | Returns true after the specified time has passed

## Actions

Actions are run when the condition is met, or if no condition is specified.

### Functions

| Function       | Argument   | Description |
|----------------|------------|-------------|
| `remove`       |            | Remove the HTML element.
| `removeParent` | Number     | Remove parent node.
| `removeIf`     | String     | Remove the HTML element if its text content matches a string.
| `addClass`     | Class name | Add a class.
| `removeClass`  | Class name | Remove a class.
| `addStyle`     | Styles     | Appends CSS to the style attribute.
| `removeStyle`  |            | Clears the style attribute.
| `click`        |            | Click the HTML element.
| `call`         |            | Call the function. Any arguments will be passed to the function.



================================================
FILE: _locales/en/messages.json
================================================
{
  "modalType": { "message": "Type" },
  "blockedModals": { "message": "Blocked" },
  "blockedModalsPage": { "message": "This page" },
  "blockedModalsTotal": { "message": "All time" },
  "modalTypes": { "message": "Modal types" },
  "modalTypeOffer": { "message": "Offers" },
  "modalTypeOfferHelp": { "message": "Promotions and discounts." },
  "modalTypePaywall": { "message": "Paywalls" },
  "modalTypePaywallHelp": { "message": "Prompts to sign up for a paid subscription." },
  "modalTypeConsent": { "message": "Consent requests" },
  "modalTypeConsentHelp": { "message": "Cookie and GDPR notices." },
  "modalTypeSignup": { "message": "Sign-up prompts" },
  "modalTypeSignupHelp": { "message": "Prompts to create an account." },
  "modalTypeEmail": { "message": "Email prompts" },
  "modalTypeEmailHelp": { "message": "Prompts to enter your email address." },
  "modalTypeDonate": { "message": "Donation requests" },
  "modalTypeDonateHelp": { "message": "Prompts to make a donation." },
  "modalTypeMessage": { "message": "Messages" },
  "modalTypeMessageHelp": { "message": "General messages and notifications." },
  "modalStatsEmpty": { "message": "No modals blocked." },
  "allowOnWebsite": { "message": "Allow modals on this website" },
  "options": { "message": "Options" },
  "optionModalTypesHelp": { "message": "Choose the modal types you wish to block." },
  "tabBlockedModals": { "message": "Blocked modals" },
  "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." },
  "tabDefinitions": { "message": "Definitions" },
  "definitionsHelp": { "message": "Did a modal not get blocked? Create your own definition here and share it." },
  "readMore": { "message": "Read more" }
}


================================================
FILE: _locales/ru/messages.json
================================================
{
  "modalType": { "message": "Тип" },
  "blockedModals": { "message": "Заблокировано" },
  "blockedModalsPage": { "message": "На этой странице" },
  "blockedModalsTotal": { "message": "За всё время" },
  "modalTypes": { "message": "Типы всплывающих сообщений" },
  "modalTypeOffer": { "message": "Акции и скидки" },
  "modalTypeOfferHelp": { "message": "Предложения купить что либо." },
  "modalTypePaywall": { "message": "Платный доступ" },
  "modalTypePaywallHelp": { "message": "Предложения оформить платную подписку." },
  "modalTypeConsent": { "message": "Уведомление о cookie" },
  "modalTypeConsentHelp": { "message": "Уведомления о cookie и GDPR." },
  "modalTypeSignup": { "message": "Предложения зарегистрироваться" },
  "modalTypeSignupHelp": { "message": "Навязчивое предложение создать аккаунт." },
  "modalTypeEmail": { "message": "Email рассылки" },
  "modalTypeEmailHelp": { "message": "Навязчивое предложение указать адрес электронной почты." },
  "modalTypeDonate": { "message": "Просьба о пожертвовании" },
  "modalTypeDonateHelp": { "message": "Предложения сделать пожертвование (задонатить)." },
  "modalTypeMessage": { "message": "Уведомления" },
  "modalTypeMessageHelp": { "message": "Назойливые сообщения и уведомления." },
  "modalStatsEmpty": { "message": "Ни одно всплывающие сообщение не заблокировано." },
  "allowOnWebsite": { "message": "Разрешить всплывающие сообщения на этом сайте" },
  "options": { "message": "Настройки" },
  "optionModalTypesHelp": { "message": "Выберите, какие всплывающие сообщения вы хотите блокировать." },
  "tabBlockedModals": { "message": "Заблокированные всплывающие сообщения" },
  "blockedModalsHelp": { "message": "Это расширение находится на ранней стадии разработки. Если всплывающие сообщение не был заблокирован, пожалуйста, сообщите об этом или создайте новое правило для его блокировки и поделитесь им." },
  "tabDefinitions": { "message": "Правила блокировки" },
  "definitionsHelp": { "message": "Всплывающие сообщение не было заблокировано? Создайте свое правило блокировки здесь и поделитесь им." },
  "readMore": { "message": "Читать подробнее" }
}


================================================
FILE: background/background.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">

		<title></title>
  </head>
  <body>
    <script src="../common/common.js"></script>
		<script src="background.js"></script>
  </body>
</html>


================================================
FILE: background/background.js
================================================
/* globals chrome, importScripts, Common */

if (typeof importScripts !== 'undefined') {
  importScripts(chrome.runtime.getURL(`common/common.js`))
}

const { modalTypes, transformDefinitions } = Common

const definitions = []

async function loadDefinitions() {
  try {
    definitions.push(
      ...(await transformDefinitions(
        await Promise.all(
          modalTypes.map(async (type) => ({
            type,
            definitions: JSON.parse(
              await (
                await fetch(chrome.runtime.getURL(`definitions/${type}.json`))
              ).text()
            ),
          }))
        )
      ))
    )
  } catch (error) {
    Background.error(error)
  }
}

const Background = {
  async getDefinitions() {
    const url = this.sender.url

    // Get custom definitions
    const { customDefinitions: definitionsByType } =
      await chrome.storage.sync.get({
        customDefinitions: {},
      })

    const customDefinitions = await transformDefinitions(
      modalTypes.map((type) => ({
        type,
        definitions: definitionsByType[type] || {},
      }))
    )

    return [...definitions, ...customDefinitions]
      .filter(({ regExps }) =>
        regExps.some((_regExp) => {
          const regExp = new RegExp(_regExp, 'i')

          return (
            regExp.test(url) ||
            regExp.test(`${url.replace(/^http(s)?:\/\//, 'http$1://www.')}`)
          )
        })
      )
      .map(({ definitions }) => definitions)
      .flat()
  },

  setBadge(text) {
    return chrome.action.setBadgeText({
      text: String(text),
      tabId: this.sender.tab.id,
    })
  },

  log(...args) {
    // eslint-disable-next-line no-console
    console.log(...args)
  },

  error(error) {
    // eslint-disable-next-line no-console
    console.error(error)
  },
}

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  const { func } = request

  if (func) {
    const args = request.args || []

    if (request.func === 'log') {
      // eslint-disable-next-line no-console
      console.log('content:', ...args)
    } else if (request.func === 'error') {
      const [message, stack] = args

      const error = new Error(`content: ${message}`)

      error.stack = stack

      Background.error(error)
    } else {
      Promise.resolve(Background[func].call({ request, sender }, ...args))
        .then((response) => {
          // eslint-disable-next-line no-console
          console.log(`content: ${func}(${args.join(', ')})`, response)

          sendResponse(response)
        })
        .catch((error) => Background.error(error))
    }
  }

  return true
})

chrome.action.setBadgeBackgroundColor({
  color: '#4755b3',
})

loadDefinitions().then(() =>
  // eslint-disable-next-line no-console
  console.log(`init ok: ${definitions.length} definitions`)
)


================================================
FILE: common/common.css
================================================
:root {
  --color-primary: #667aff;
  --color-primary-dark: #4755b3;
  --color-primary-light: #f1f3ff;
  --color-heading: #333;
  --color-text: #4a4a4a;
  --color-grey: #ccc;
  --color-grey-light: #f4f4f4;
  --color-white: #fff;
  --radius: 5px;
}

*, *::before, *::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

body {
  background-color: var(--color-white);
  color: var(--color-text);
  direction: __MSG_@@bidi_dir__;
	font-family: Helvetica, Arial, sans-serif;
  font-size: .9rem;
  line-height: 1.5rem;
  -webkit-font-smoothing: antialiased;
}

p {
  margin-bottom: 1rem;
}

a {
  color: var(--color-primary);
}

input, button, textarea, select {
  background-color: var(--color-white);
  font: inherit;
}

input[type="text"], textarea, select {
  display: block;
  padding: .5rem;
  border: 1px solid var(--color-grey);
  border-radius: var(--radius);
  margin-bottom: 1rem;
  width: 100%;
}

input[type="checkbox"], input[type="radio"] {
  margin-right: .5rem;
}

textarea.code {
  font-family: Monaco, monospace;
  font-size: .7rem;
  line-height: 1.1rem;
}

h2 {
  color: var(--color-heading);
  font-size: .9rem;
  margin-bottom: .5rem;
}

table {
  border-collapse: collapse;
  border-style: hidden;
  border-spacing: 0;
  font-size: .8rem;
  width: 100%;
}

th {
  color: var(--color-heading);
  text-align: left;
}

th, td {
  padding: .25rem .5rem;
}

th:first-child, td:first-child {
  border-radius: var(--radius) 0 0 var(--radius);
}

th:last-child, td:last-child {
  border-radius: 0 var(--radius) var(--radius) 0;
}

tr:first-child th,
tr:nth-child(even) td {
  background: var(--color-primary-light);
}

hr {
  border-color: var(--color-grey);
  border-style: solid;
  border-width: 1px 0 0 0;
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.label {
  font-weight: bold;
}

.checkbox label {
  display: flex;
  align-items: center;
  margin-bottom: .5rem;
}

.radio label {
  display: inline-flex;
  align-items: center;
  margin-right: .5rem;
}

.help {
  font-size: .8rem;
  margin-bottom: .5rem;
}

.input-error {
  color: #ff5353;
  font-size: .8rem;
  margin: -.5rem 0 .5rem 0;
}

.input-error:empty {
  margin: 0;
}

.hidden {
  display: none;
}

.icon {
  color: #fff;
  height: 1.1rem;
  vertical-align: middle;
  width: 1.1rem;
}

.container {
  margin: 1rem;
}

.tabs {
  border-bottom: 1px solid var(--color-grey);
  display: flex;
}

.tab {
  border-bottom: 2px solid transparent;
  color: var(--color-primary);
  cursor: pointer;
  padding: .8rem 1rem;
  margin-bottom: -1px;
}

.tab:hover {
  border-bottom-color: var(--color-primary);
}

.tab--active {
  border-bottom-color: var(--color-primary);
}

.alert {
  background-color: #ffffcc;
  color: #999900;
  font-size: .8rem;
  padding: 1rem;
}

.alert a, .alert a:hover, .alert a:active, .alert a:visited {
  color: #999900;
}


================================================
FILE: common/common.js
================================================
/* globals chrome */

// Manifest v2 polyfill
if (chrome.runtime.getManifest().manifest_version === 2) {
  chrome.action = chrome.browserAction

  chrome.storage.sync = {
    get: (...args) =>
      new Promise((resolve) => chrome.storage.local.get(...args, resolve)),
    set: (...args) =>
      new Promise((resolve) => chrome.storage.local.set(...args, resolve)),
  }
}

const Common = {
  modalTypes: [
    'offer',
    'paywall',
    'email',
    'signup',
    'consent',
    'donate',
    'message',
  ],

  $:
    typeof window !== 'undefined'
      ? document.querySelector.bind(document)
      : () => {},
  $$:
    typeof window !== 'undefined'
      ? document.querySelectorAll.bind(document)
      : () => {},

  isObject(object) {
    return typeof object === 'object' && object && !Array.isArray(object)
  },

  arrayify(item) {
    return Array.isArray(item) ? item : [item]
  },

  debounce(func, wait) {
    let timeout

    return (...args) => {
      const debounced = () => {
        clearTimeout(timeout)

        func(...args)
      }

      clearTimeout(timeout)

      timeout = setTimeout(debounced, wait)
    }
  },

  i18n() {
    const elements = Common.$$('[data-i18n]')

    elements.forEach((element) => {
      element.textContent = chrome.i18n.getMessage(element.dataset.i18n)
    })
  },

  capitalize(string) {
    return string.charAt(0).toUpperCase() + string.slice(1)
  },

  removeChildren(element) {
    while (element.firstChild) {
      element.removeChild(element.firstChild)
    }
  },

  el(name) {
    return document.createElement(name)
  },

  getActiveTab() {
    return new Promise((resolve) => {
      chrome.tabs.query({ active: true, currentWindow: true }, (tabs) =>
        resolve(tabs[0])
      )
    })
  },

  async tokenify(string) {
    const tokens = []

    let bracketLevel = 0
    let quote = false
    let token = ''

    for (const char of string.split('')) {
      token += char

      if (token === 'if ') {
        token = ''
      }

      switch (char) {
        case "'":
          if (!bracketLevel) {
            quote = !quote

            if (!quote) {
              tokens.push(token.replace(/(^'|'$)/g, ''))

              token = ''
            }
          }

          break
        case '(':
          if (!quote) {
            bracketLevel++
          }

          break
        case ')':
          if (!quote) {
            bracketLevel--

            if (!bracketLevel) {
              const [, func, arg] = token.trim().match(/([^(]+)\((.+)\)$/)

              if (!Common.Functions[func]) {
                throw new Error(`Function does not exist: ${func}`)
              }

              try {
                await Common.Functions[func].call({ validateOnly: true }, arg)
              } catch (error) {
                throw new Error(`Invalid argument: ${arg}`)
              }

              tokens.push({ func, arg })

              token = ''
            }
          }

          break
      }
    }

    if (token) {
      tokens.push(token)
    }

    return tokens
  },

  async transformDefinitions(definitionsByType) {
    const definitionsByGlob = []

    for (const { type, definitions } of definitionsByType) {
      if (!Common.isObject(definitions)) {
        throw new TypeError(
          `Unexpected definitions type, expected object: ${definitions}`
        )
      }

      for (const glob of Object.keys(definitions)) {
        definitionsByGlob[glob] = definitionsByGlob[glob] || {
          glob,
          regExps: Common.globToRegExp(glob),
          definitions: [],
        }

        definitionsByGlob[glob].definitions.push(
          ...(
            await Promise.all(
              Common.arrayify(definitions[glob]).map((definition) => {
                try {
                  if (!Common.isObject(definition)) {
                    throw new TypeError(
                      `Unexpected definition type, expected object: ${definition}`
                    )
                  }

                  return Promise.all(
                    Object.keys(definition).map(async (key) => {
                      const conditions = key.startsWith('if ')
                        ? await Common.tokenify(key)
                        : null

                      if (conditions && !conditions.length) {
                        throw new Error(`Invalid condition: ${key}`)
                      }

                      let actions = []

                      if (conditions) {
                        if (!Common.isObject(definition[key])) {
                          throw new TypeError(
                            `Invalid actions type, expected object: ${definition[key]}`
                          )
                        }

                        actions = Object.keys(definition[key]).map(
                          (selector) => ({
                            selector,
                            action: definition[key][selector],
                          })
                        )
                      } else {
                        actions = [{ selector: key, action: definition[key] }]
                      }

                      actions = await Promise.all(
                        actions.map(async ({ selector, action }) => {
                          if (typeof action !== 'string') {
                            throw new TypeError(
                              `Unexpected action type, expected string`
                            )
                          }

                          const [func, ...splitArgs] = action.split(' ')

                          const args = await Common.tokenify(
                            splitArgs.join(' ')
                          )

                          return { selector, func, args }
                        })
                      )

                      actions.forEach(({ selector, func, args }) => {
                        try {
                          Common.$(selector)
                        } catch (error) {
                          throw new Error(
                            `Invalid action selector: ${selector}`
                          )
                        }

                        if (!Common.Actions[func]) {
                          throw new Error(
                            `Invalid action function: ${func} (${selector})`
                          )
                        }
                      })

                      return { type, conditions: conditions || [], actions }
                    })
                  )
                } catch (error) {
                  throw new Error(`${error.message || error} in ${glob}`)
                }
              })
            )
          ).flat()
        )
      }
    }

    return Object.values(definitionsByGlob).flat()
  },

  globToRegExp(glob) {
    const globs = glob.split(' ')

    return globs.map((glob) => {
      try {
        if (glob !== '*') {
          if (!glob.includes('.')) {
            throw new Error('Invalid glob')
          }

          // eslint-disable-next-line no-new
          new URL(`https://${glob.replace('*', 'test')}`)
        }

        return new RegExp(
          glob === '*'
            ? '^https?://.+'
            : `^https?://${glob.replace('.', '\\.').replace('*', '[^./]+')}\\b`
        ).source
      } catch (error) {
        throw new Error(`Invalid URL pattern: ${glob}`)
      }
    })
  },

  // Run a script in the context of the page
  inject(func, ...args) {
    return new Promise((resolve) => {
      const uid = Math.floor(Math.random() * 900000) + 100000

      const script = Common.el('script')

      script.src = chrome.runtime.getURL('inject/inject.js')

      script.dataset.demodal = 'true'

      script.onload = () => {
        const receiveMessage = ({ data: { demodalResponse } }) => {
          if (!demodalResponse || demodalResponse.uid !== uid) {
            return
          }

          window.removeEventListener('message', receiveMessage)

          script.remove()

          resolve(demodalResponse.message)
        }

        window.addEventListener('message', receiveMessage)

        window.postMessage({ demodalRequest: { func, args, uid } })
      }

      document.body.append(script)
    })
  },

  Actions: {
    remove() {
      this.remove()

      return true
    },
    removeParent(level = 1) {
      let node = this

      for (let index = 0; index < level; index++) {
        node = node ? node.parentNode : null
      }

      if (node) {
        node.remove()

        return true
      } else {
        return false
      }
    },
    removeIf(...args) {
      if (
        args.every((string) =>
          this.textContent.toLowerCase().includes(string.toLowerCase().trim())
        )
      ) {
        this.remove()

        return true
      }

      return false
    },
    addClass(...args) {
      this.classList.add(...args)

      return true
    },
    removeClass(...args) {
      if (args[0] === '*') {
        this.className = ''
      } else {
        this.classList.remove(...args)
      }

      return true
    },
    addStyle(...args) {
      this.style = `${this.style}; ${args[0]}`

      return true
    },
    removeStyle() {
      this.style = ''

      return true
    },
    click() {
      this.click()

      return true
    },
    call(...args) {
      Common.inject('call', ...args)

      return true
    },
  },

  Functions: {
    $(selector) {
      try {
        return !!Common.$(selector)
      } catch (error) {
        throw new Error(`Invalid selector: ${selector}`)
      }
    },
    async defined(...args) {
      if (this.validateOnly) {
        return true
      }

      return await Common.inject('defined', ...args)
    },
    sleep(ms = 0) {
      if (this.validateOnly) {
        return true
      }

      return new Promise((resolve) =>
        setTimeout(() => resolve(true), parseInt(ms, 10))
      )
    },
  },

  Background: {
    call(func, ...args) {
      return new Promise((resolve, reject) =>
        chrome.runtime.sendMessage({ func, args }, (response) =>
          chrome.runtime.lastError
            ? reject(new Error(chrome.runtime.lastError.message))
            : resolve(response)
        )
      )
    },
  },

  Content: {
    call(func, ...args) {
      return new Promise((resolve, reject) =>
        Common.getActiveTab().then((tab) =>
          chrome.tabs.sendMessage(tab.id, { func, args }, (response) =>
            chrome.runtime.lastError
              ? reject(new Error(chrome.runtime.lastError.message))
              : resolve(response)
          )
        )
      )
    },
  },
}


================================================
FILE: content/content.js
================================================
/* eslint-env browser */
/* globals chrome, Common */

const { $, debounce, Background, Actions, Functions } = Common

const definitions = []

const blockedModals = {}

// Functions that can be called from the popup script
const Content = {
  getBlockedModals() {
    return blockedModals
  },

  getUrl() {
    return window.location.href
  },

  reload() {
    window.location.reload()
  },

  async isAllowed(url = window.location.href) {
    // Check if hostname is in allow list
    const { allowList } = await chrome.storage.sync.get({
      allowList: [],
    })

    let { hostname } = new URL(url)

    hostname = hostname.replace(/^www\./, '')

    return allowList.includes(hostname)
  },
}

// Log messages in the background script console
function log(...messages) {
  const error = messages[0]

  if (error instanceof Error) {
    Background.call('error', error.toString(), error.stack)
  } else {
    Background.call('log', ...messages)
  }
}

const run = async () => {
  try {
    if (await Content.isAllowed()) {
      return
    }

    await Promise.all(
      definitions.map(async (definition) => {
        const { type, conditions, actions, completed } = definition

        if (
          completed ||
          !(
            await Promise.all(
              conditions.map(({ func, arg }) => Functions[func](arg))
            )
          ).every((result) => result)
        ) {
          return
        }

        let found = false

        actions.forEach(({ selector, func, args }) => {
          let success = false

          switch (func) {
            case 'call':
              success = Actions[func](selector, ...args)

              break
            default:
              // eslint-disable-next-line no-case-declarations
              const node = $(selector)

              if (node) {
                success = Actions[func].call(node, ...args)
              }
          }

          if (success) {
            log(`action: ${selector}: ${func}(${args.join(', ')})`)
          }

          found = found || success
        })

        if (found) {
          definition.completed = true

          blockedModals[type] = (blockedModals[type] || 0) + 1

          Background.call(
            'setBadge',
            Object.values(blockedModals).reduce((sum, value) => sum + value, 0)
          )

          // Update all-time totals
          chrome.storage.sync
            .get({
              blockedModals: {},
            })
            .then(({ blockedModals }) => {
              blockedModals[type] = (blockedModals[type] || 0) + 1

              chrome.storage.sync.set({ blockedModals })
            })
        }
      })
    )
  } catch (error) {
    log(error)
  }
}

// Listen for messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  const { func } = request

  if (func) {
    const args = request.args || []

    Promise.resolve(Content[func].call({ request, sender }, ...args))
      .then((response) => {
        // eslint-disable-next-line no-console
        log(`popup: ${func}(${args.join(', ')})`, response)

        sendResponse(response)
      })
      .catch((error) => log(error))
  }

  return true
})

//
;(async () => {
  try {
    definitions.push(...(await Background.call('getDefinitions')))

    const runDebounced = debounce(() => run(), 500)

    const mutationObserver = new MutationObserver((mutations) => {
      // Avoid infinite loop if mutation was done by us
      if (
        mutations.every((mutation) =>
          Array.from(mutation.addedNodes).every((node) => node.dataset.demodal)
        )
      ) {
        return
      }

      runDebounced()
    })

    mutationObserver.observe(document.body, { subtree: true, childList: true })

    run()
  } catch (error) {
    log(error)
  }
})()


================================================
FILE: definitions/consent.json
================================================
{
  "*": {
    "#cookie-law-info-bar": "remove",
    "#moove_gdpr_cookie_info_bar": "remove",
    "if defined(BorlabsCookie)": {
      "BorlabsCookie.hideCookieBox": "call"
    },
    "#cookie-banner": "remove",
    ".cookie-banner": "remove",
    "#onetrust-consent-sdk": "remove",
    "#CybotCookiebotDialogBodyButtonDecline": "click",
    ".cookie-bar__close": "click",
    "[aria-label='cookieconsent']": "remove",
    ".cookie-warning-modal": "remove",
    ".cookie-consent": "remove",
    "#cookie-notice": "remove",
    ".cookie-notice-big": "remove",
    "#cookie-bar": "remove",
    "if $(#didomi-host)": {
      "#didomi-host": "remove",
      "body": "removeClass didomi-popup-open"
    },
    "#cookieBanner": "remove",
    "#cconsent-bar": "remove",
    ".CookieConsent": "remove",
    "#gdpr-banner": "remove",
    "#gdpr-cookie-message": "remove",
    ".cookie-notice": "remove",
    ".cc-banner": "remove",
    ".js-cookie-consent-overlay": "remove",
    "#ccc": "remove",
    "#iubenda-cs-banner": "remove",
    ".wppopups-whole": "removeIf 'cookies'",
    ".gdpr-disclaimer": "remove",
    "._gdprDisclaimer": "remove",
    ".ce-cookie-banner": "remove",
    ".cookie-msg": "remove",
    ".cookie-panel": "remove",
    ".cookie-popup": "remove",
    "#cookie-popup-wrapper": "remove",
    "[data-component='CookieBanner']": "remove",
    ".t-consentPrompt": "remove",
    "#privacy-consent": "remove",
    "if $(#acceptationCMPWall)": {
      "#acceptationCMPWall": "remove",
      "body": "removeStyle"
    },
    ".cookies_message": "remove",
    "#cookieConsent": "remove",
    ".cookie-policy": "remove",
    "#cookieNoticeAlert": "remove"
  },
  "*.stackexchange.com *.superuser.com *.stackoverflow.com *.mathoverflow.net *.serverfault.com *.askubuntu.com *.stackapps.com": {
    ".js-consent-banner": "remove"
  },
  "*.deepl.com": {
    "#dl_cookieBanner": "remove"
  },
  "*.lego.com": {
    ".cookies-used-notice": "remove"
  },
  "*.redhat.com": {
  	"#truste-consent-track": "remove",
  	".redhat-cookie-banner": "remove"
  },
  "*.openweathermap.org": {
  	"#stick-footer-panel": "remove"
  }
}


================================================
FILE: definitions/donate.json
================================================
{
  "*.theguardian.com": {
    ".site-message--banner": "remove",
    "#bottom-banner": "removeIf 'Support the Guardian'"
  },
  "*.science.org": {
    ".alert-donation": "remove"
  }
}


================================================
FILE: definitions/email.json
================================================
{
  "*": {
    "if $(.elementor-popup-modal)": {
      ".elementor-popup-modal": "remove",
      "body": "removeClass dialog-prevent-scroll"
    },
    "if $(.sgpb-popup-dialog-main-div-wrapper input[type='email'])": {
      ".sgpb-popup-dialog-main-div-wrapper": "remove"
    },
    "if $([aria-modal='true'] .klaviyo-close-form)": {
      "[aria-modal='true'] .klaviyo-close-form": "click"
    },
    "#shopify-section-newsletter-popup .modal__close": "click",
    ".close-newsletter": "click",
    ".cp-popup-container": "removeIf 'subscribe'",
    ".js_modal_exit_intent": "removeIf 'subscribe'"
  },
  "*.npr.org": {
    ".newsletter-stickybar": "remove"
  },
  "*.redhat.com": {
  	".subscribe-sidebar": "remove"
  }
}


================================================
FILE: definitions/message.json
================================================
{
  "*": {
    "if $([id^='sp_message_container_'])": {
      "[id^='sp_message_container_']": "remove",
      "html": "removeClass sp-message-open"
    }
  },
  "*.heraldsun.com.au": {
    ".DialogBox": "remove"
  },
  "*.simplywall.st": {
    "if $([data-cy-id='careers-upsell'])": {
      "[data-cy-id='careers-upsell']": "removeParent"
    }
  }
}



================================================
FILE: definitions/offer.json
================================================
{
  "*.forbes.com": {
    "[external-event='offer-close-modal']": "click"
  },
  "*.theconversation.com": {
    ".promo": "remove"
  }
}


================================================
FILE: definitions/paywall.json
================================================
{
  "*.nytimes.com": {
    "if $(#gateway-content)": {
      "#gateway-content": "remove",
      "#app > div > div": "removeClass *",
      "#app > div > div > div:last-child": "remove",
      "#site-content": "removeStyle"
    }
  },
  "*.newyorker.com": {
    ".paywall-bar": "remove",
    ".paywall-modal": "remove"
  },
  "*.wired.com": {
    ".persistent-bottom": "remove"
  },
  "*.washingtonpost.com": {
    "if $(.paywall-overlay) sleep(1000)": {
      ".paywall-overlay": "remove",
      "html": "removeStyle",
      "body": "addStyle 'overflow: inherit; position: inherit;'"
    },
    ".softwall-overlay": "remove",
    "[id^='softwall-']": "remove"
  },
  "*.simplywall.st": {
    "if $(#modal-container)": {
      "#modal-container": "remove",
      "#root": "addStyle filter: none"
    }
  },
  "*.theguardian.com": {
    "#bottom-banner": "removeIf 'Start free trial'"
  },
  "*.bloomberg.com": {
    "if $(#fortress-paywall-container-root)": {
      "#fortress-paywall-container-root": "remove",
      "body": "addStyle overflow: auto"
    }
  },
  "*.nikkei.com": {
    ".pw-widget--benefit-pop-up .pianoj-ribbon-close": "click"
  },
  "*.telegraph.co.uk": {
    ".martech-modal-component-overlay": "remove"
  },
  "*.nationalgeographic.com": {
    "if $(.EmailStickyFooter__Modal)": {
      ".EmailStickyFooter__Modal": "remove",
      ".Scroll--locked": "removeClass Scroll--locked",
      "body": "removeStyle"
    }
  }
}


================================================
FILE: definitions/signup.json
================================================
{
  "*": {
    ".wbounce-modal": "removeIf 'newsletter'",
    ".leadinModal": "remove",
    ".wppopups-whole": "removeIf 'sign up'",
    ".exit-intent": "removeIf 'sign up'",
    ".exit-intent-modal": "remove",
    "[id^='smsbump-form']": "remove",
    "#ouibounce-modal": "remove"
  },
  "*.smh.com.au *.theage.com.au": {
    "[data-testid='registration-prompt']": "remove",
    "#registrationWall": "remove"
  },
  "*.ishka.com.au": {
    "if $(.sign_up_karma_club_popup) defined(CloseClubPoup)": {
      "CloseClubPoup": "call"
    }
  },
  "*.nytimes.com": {
    ".MAG_web_all_Monthly-Sale-dock": "remove"
  },
  "*.theatlantic.com": {
    "#paywall[data-category^='nudge']": "remove"
  },
  "*.bloomberg.com": {
    "#fortress-preblocked-container-root": "remove"
  },
  "*.ieee.org": {
    ".lightbox-popup": "removeIf 'create an account'"
  },
  "*.afr.com": {
    "if $([data-testid='SubscriptionPrompt-close'])": {
      "[data-testid='SubscriptionPrompt-close']": "click"
    }
  },
  "*.pinterest.com *.pinterest.com.au": {
    "if $([data-test-id='giftWrap'])": {
      "[data-test-id='giftWrap']": "remove",
      "body": "removeStyle"
    }
  },
  "*.vic.gov.au": {
    "#subscribe-banner-react": "remove"
  },
  "*.nature.com": {
    ".c-site-messages--nature-briefing-email-variant": "remove"
  },
  "*.boredpanda.com": {
    "if $(#subscribe-before-leaving)": {
      "#subscribe-before-leaving": "remove",
      "#modal-backdrop": "remove",
      "body": "removeClass modal-open"
    }
  },
  "*.bbc.com": {
    "if $(.tp-modal-open)": {
      "body": "removeClass tp-modal-open",
      ".tp-modal": "remove",
      ".tp-backdrop": "remove"
    }
  },
  "*.ibtimes.com": {
    ".grwf2_backdrop": "remove",
    ".wf2-popover": "remove"
  },
  "vk.com": {
  	"#page_bottom_banners_root": "remove",
  	"if $(.UnauthActionBox)": {
  		".UnauthActionBox__close": "click"
  	}
  }
}


================================================
FILE: inject/inject.js
================================================
/* eslint-env browser */

;(function () {
  try {
    const chainToProp = (chain) => {
      return chain
        .split('.')
        .reduce(
          (value, method) =>
            value &&
            value instanceof Object &&
            Object.prototype.hasOwnProperty.call(value, method)
              ? value[method]
              : undefined,
          window
        )
    }

    const Functions = {
      defined(chain) {
        return chainToProp(chain) !== undefined
      },
      call(chain, ...args) {
        chainToProp(chain)?.(...args)
      },
    }

    const receiveMessage = ({ data: { demodalRequest } }) => {
      if (!demodalRequest) {
        return
      }

      const { func, args, uid } = demodalRequest

      removeEventListener('message', receiveMessage)

      postMessage({
        demodalResponse: {
          uid,
          message:
            func && Functions[func] ? Functions[func](...(args || [])) : false,
        },
      })
    }

    addEventListener('message', receiveMessage)
  } catch (error) {
    // Fail quietly
  }
})()


================================================
FILE: manifest-v2.json
================================================
{
  "name": "Demodal - Block modals and overlays",
  "description": "Demodal automatically removes content blocking modals including paywalls, discount offers, promts to sign up or enter your email address and more.",
  "version": "1.0.3",
  "manifest_version": 2,
  "default_locale": "en",
  "background": {
    "page": "background/background.html"
  },
  "content_scripts": [
    {
      "matches": ["https://*/*", "http://*/*"],
      "js": [
        "common/common.js",
        "content/content.js"
      ]
    }
  ],
  "permissions": ["storage", "activeTab"],
  "browser_action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "128": "images/icon-128.png",
      "256": "images/icon-256.png"
    }
  },
  "options_ui":{
    "page": "options/options.html",
    "open_in_tab": true
  },
  "icons": {
    "128": "images/icon-128.png",
    "256": "images/icon-256.png"
  }
}


================================================
FILE: manifest-v3.json
================================================
{
  "name": "Demodal - Block modals and overlays",
  "description": "Demodal automatically removes content blocking modals including paywalls, discount offers, email address prompts and more.",
  "version": "1.0.3",
  "manifest_version": 3,
  "default_locale": "en",
  "background": {
    "service_worker": "background/background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://*/*", "http://*/*"],
      "js": [
        "common/common.js",
        "content/content.js"
      ]
    }
  ],
  "permissions": ["storage", "activeTab", "scripting"],
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "128": "images/icon-128.png",
      "256": "images/icon-256.png"
    }
  },
  "options_page": "options/options.html",
  "icons": {
    "128": "images/icon-128.png",
    "256": "images/icon-256.png"
  }
}


================================================
FILE: options/options.css
================================================
.container {
  max-width: 600px;
  margin-left: auto;
  margin-right: auto;
}


================================================
FILE: options/options.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">

		<title data-i18n="options"></title>

    <link rel="stylesheet" href="../common/common.css">
    <link rel="stylesheet" href="options.css">
  </head>
  <body>
    <div class="container">
      <div class="card">
      <div class="card__content">
        <h2 data-i18n="modalTypes"></h2>

        <p data-i18n="optionModalTypesHelp"><p>

        <div id="modal-types"></div>
      </div>
      </div>
    </div>

    <script src="../common/common.js"></script>
		<script src="options.js"></script>
  </body>
</html>


================================================
FILE: options/options.js
================================================
/* globals chrome, Common */

const { $, i18n, el, capitalize, modalTypes } = Common

i18n()

//
;(async () => {
  const { optionBlockModalTypes } = await chrome.storage.sync.get({
    optionBlockModalTypes: modalTypes.reduce(
      (options, type) => ({ ...options, [type]: true }),
      {}
    ),
  })

  modalTypes.forEach((type) => {
    const div1 = el('div')
    const label = el('label')
    const checkbox = el('input')
    const div2 = el('div')

    div1.className = 'checkbox'

    checkbox.type = 'checkbox'

    checkbox.checked = optionBlockModalTypes[type]

    checkbox.addEventListener('change', () => {
      optionBlockModalTypes[type] = checkbox.checked

      chrome.storage.sync.set({ optionBlockModalTypes })
    })

    div2.textContent = chrome.i18n.getMessage(
      `modalType${capitalize(type)}Help`
    )

    div2.className = 'help'

    label.append(
      checkbox,
      chrome.i18n.getMessage(`modalType${capitalize(type)}`)
    )

    div1.append(label, div2)

    $('#modal-types').append(div1)
  })
})()


================================================
FILE: package.json
================================================
{
  "devDependencies": {
    "@nuxtjs/eslint-config": "^3.1.0",
    "@nuxtjs/eslint-module": "^2.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^7.13.0",
    "eslint-config-prettier": "^6.15.0",
    "eslint-plugin-json": "^2.1.2",
    "eslint-plugin-nuxt": "^1.0.0",
    "eslint-plugin-prettier": "^3.1.4",
    "prettier": "^2.1.2"
  }
}


================================================
FILE: popup/popup.css
================================================
body {
  width: 500px;
}

.header {
  background-color: var(--color-primary);
  display: flex;
  align-items: center;
  padding: 1rem;
}

.header__logo {
  margin-right: .8rem;
  width: 24px;
}

.header__title {
  color: var(--color-white);
  font-size: .9rem;
  text-transform: uppercase;
}

.header__links {
  text-align: right;
  flex: 1 0;
}

.content {
  opacity: .5;
}

.content.visible {
  opacity: 1;
}

#input-definitions {
  height: 300px;
}

#input-debug {
  height: 300px;
}


================================================
FILE: popup/popup.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">

    <title></title>

    <link rel="stylesheet" href="../common/common.css">
    <link rel="stylesheet" href="popup.css">
  </head>
  <body>
    <div class="header">
      <img class="header__logo" src="../images/icon-mono-256.png" alt="" />

      <h1 class="header__title">Demodal</h1>

      <div class="header__links">
        <a class="link-options" href="#">
          <svg class="icon" viewBox="0 0 24 24">
            <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" />
          </svg>
        </a>
      </div>
    </div>

    <div class="tabs">
      <div class="tab tab--active" data-tab="blocked-modals" data-i18n="tabBlockedModals"></div>
      <div class="tab" data-tab="definitions" data-i18n="tabDefinitions"></div>
    </div>

    <div class="content">
      <div data-tab-content="blocked-modals">
        <div class="alert">
          <span data-i18n="blockedModalsHelp"></span>
          <a href="https://github.com/AliasIO/demodal/blob/master/README.md" target="_blank" data-i18n="readMore"></a>
        </div>

        <div class="if-connected">
          <div id="options" class="container">
            <div class="checkbox">
              <label>
                <input id="input-allowed" type="checkbox"> <span data-i18n="allowOnWebsite"></span>
              </label>
            </div>
          </div>

          <hr />

          <div id="blocked-page" class="container">
            <h2 data-i18n="blockedModalsPage"></h2>

            <div id="blocked-page__missing" class="hidden" data-i18n="modalStatsEmpty"></div>

            <table id="blocked-page__stats">
              <thead>
                <tr>
                  <th width="50%">
                    <span data-i18n="modalType"></span>
                  </th>
                  <th width="50%">
                    <span data-i18n="blockedModals"></span>
                  </th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
        </div>

        <div id="blocked-total" class="container">
          <h2 data-i18n="blockedModalsTotal"></h2>

          <div id="blocked-total__missing" class="hidden" data-i18n="modalStatsEmpty"></div>

          <table id="blocked-total__stats">
            <thead>
              <tr>
                <th width="50%">
                  <span data-i18n="modalType"></span>
                </th>
                <th width="50%">
                  <span data-i18n="blockedModals"></span>
                </th>
              </tr>
            </thead>
            <tbody></tbody>
          </table>
        </div>
      </div>

      <div class="hidden" data-tab-content="definitions">
        <div class="container">
          <p>
          <span data-i18n="definitionsHelp"></span>
          <a href="https://github.com/AliasIO/demodal/blob/master/README.md" target="_blank" data-i18n="readMore"></a>
          </p>

          <select id="input-modal-types"></select>

          <textarea id="input-definitions" class="code"></textarea>

          <textarea id="input-debug" class="code hidden" readonly></textarea>

          <div id="errors-definitions" class="input-error"></div>

          <div class="row">
            <div class="radio">
              <label>
                <input type="radio" name="input-mode" value="edit" checked /> Edit
              </label>

              <label>
                <input type="radio" name="input-mode" value="debug" /> Debug
              </label>
            </div>

            <div class="text-right">
              <a href="#" class="reload">Reload page</a>
            </div>
          </div>
        </div>
      </div>
    </div>

    <script src="../common/common.js"></script>
    <script src="popup.js"></script>
  </body>
</html>


================================================
FILE: popup/popup.js
================================================
/* globals chrome, Common */

const {
  $,
  $$,
  i18n,
  capitalize,
  removeChildren,
  el,
  debounce,
  modalTypes,
  transformDefinitions,
  Content,
} = Common

function renderTotals(prefix, totals) {
  let any = false

  removeChildren($(`${prefix}__stats tbody`))

  modalTypes.forEach((type) => {
    const tr = el('tr')
    const td1 = el('td')
    const td2 = el('td')

    if (totals[type]) {
      td1.textContent = chrome.i18n.getMessage(`modalType${capitalize(type)}`)
      td2.textContent = totals[type]

      tr.appendChild(td1)
      tr.appendChild(td2)
      $(`${prefix}__stats tbody`).appendChild(tr)

      any = true
    }
  })

  if (any) {
    $(`${prefix}__stats`).classList.remove('hidden')
    $(`${prefix}__missing`).classList.add('hidden')
  }
}

i18n()

//
;(async () => {
  $('.content').classList.remove('visible')

  $(`#blocked-page__stats`).classList.add('hidden')
  $(`#blocked-page__missing`).classList.remove('hidden')
  $(`#blocked-total__stats`).classList.add('hidden')
  $(`#blocked-total__missing`).classList.remove('hidden')

  $$('.tab').forEach((tab) =>
    tab.addEventListener('click', (e) => {
      $$('.tab').forEach((tab) => tab.classList.remove('tab--active'))

      e.target.classList.add('tab--active')

      $$('[data-tab-content]').forEach((tabContent) =>
        tabContent.classList.add('hidden')
      )

      $(`[data-tab-content='${e.target.dataset.tab}']`).classList.remove(
        'hidden'
      )
    })
  )

  // Blocked modals tab

  $('.link-options').addEventListener('click', (e) => {
    e.preventDefault()

    chrome.runtime.openOptionsPage()
  })

  // Show all-time totals
  const { blockedModals: totals } = await chrome.storage.sync.get({
    blockedModals: {},
  })

  renderTotals('#blocked-total', totals)

  let connected = true

  try {
    // Show page totals
    const blockedModals = await Content.call('getBlockedModals')

    renderTotals('#blocked-page', blockedModals)
  } catch (error) {
    // eslint-disable-next-line no-console
    console.error(error)

    connected = false
  }

  // Show page specific content only if we have a content script connection
  $$('.if-connected').forEach((element) =>
    element.classList[connected ? 'remove' : 'add']('hidden')
  )

  if (connected) {
    // Add/remove hostnames to allow list
    const allowed = await Content.call('isAllowed')

    $('#input-allowed').checked = allowed

    $('#input-allowed').addEventListener('click', async (el) => {
      const { allowList } = await chrome.storage.sync.get({
        allowList: [],
      })

      const url = await Content.call('getUrl')

      let { hostname } = new URL(url)

      hostname = hostname.replace(/^www\./, '')

      if (el.target.checked) {
        allowList.push(hostname)
      } else {
        const index = allowList.findIndex((_hostname) => _hostname === hostname)

        if (index !== -1) {
          allowList.splice(index, 1)
        }
      }

      chrome.storage.sync.set({ allowList })

      Content.call('reload')
    })
  }

  // Definitions tab

  // Modal type select
  modalTypes.forEach((type) => {
    const option = el('option')

    option.value = type
    option.textContent = chrome.i18n.getMessage(`modalType${capitalize(type)}`)

    $('#input-modal-types').append(option)
  })

  $('#input-modal-types').addEventListener('change', (event) => {
    const modalType = event.target.value

    $('#errors-definitions').textContent = ''

    chrome.storage.sync
      .get({
        customDefinitions: {},
      })
      .then(
        ({ customDefinitions }) =>
          ($('#input-definitions').value = JSON.stringify(
            customDefinitions[modalType] || {},
            null,
            2
          ))
      )
  })

  $('#input-modal-types').dispatchEvent(new Event('change'))

  // Edit / debug toggle
  $$('input[name="input-mode"]').forEach((el) =>
    el.addEventListener('change', (event) => {
      const debug = event.target.value === 'debug'

      $('#input-definitions').classList[debug ? 'add' : 'remove']('hidden')
      $('#input-debug').classList[debug ? 'remove' : 'add']('hidden')
    })
  )

  // Reload page
  $('.reload').addEventListener('click', () => Content.call('reload'))

  // Format JSON on blur
  $('#input-definitions').addEventListener('blur', (event) => {
    const json = event.target.value || '{}'

    try {
      const definitions = JSON.parse(json)

      event.target.value = JSON.stringify(definitions, null, 2)

      const selectedType = $('#input-modal-types').value

      chrome.storage.sync
        .get({
          customDefinitions: modalTypes.reduce(
            (definitions, type) => ({ ...definitions, [type]: {} }),
            {}
          ),
        })
        .then(({ customDefinitions }) =>
          chrome.storage.sync.set({
            customDefinitions: {
              ...customDefinitions,
              [selectedType]: definitions,
            },
          })
        )
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error(error)
    }
  })

  // Validate JSON on change
  $('#input-definitions').addEventListener(
    'input',
    debounce((event) => {
      const json = event.target.value || '{}'

      try {
        const definitionsByType = [
          {
            type: 'offers',
            definitions: JSON.parse(json),
          },
        ]

        const definitions = transformDefinitions(definitionsByType)

        $('#input-debug').textContent = JSON.stringify(definitions, null, 2)

        $('#errors-definitions').textContent = ''
      } catch (error) {
        // eslint-disable-next-line no-console
        console.error(error)

        $('#errors-definitions').textContent = error.message || error.toString()
      }
    }, 500)
  )

  $('.content').classList.add('visible')
})()


================================================
FILE: run
================================================
#!/bin/bash

case $1 in
  manifest)
    if [[ -f manifest-$2.json ]]; then
      cat manifest-$2.json > manifest.json

      echo Switched to manifest $2
    else
      echo Invalid argument $2
    fi
    ;;

  build)
    version=$2

    if [ -z "$version" ]; then
      echo "No version specified"

      exit
    fi

    sed -i '' -r "s/\"version\": \".+\"/\"version\": \"$2\"/" manifest-v2.json
    sed -i '' -r "s/\"version\": \".+\"/\"version\": \"$2\"/" manifest-v3.json

    find . -name '.DS_Store' -type f -delete

    files="_locales background common content definitions images inject options popup manifest.json"

    cp manifest.json manifest.json.bak

    cat manifest-v2.json > manifest.json

    rm build/*

    zip -r build/demodal-v2.zip $files

    cat manifest-v3.json > manifest.json

    zip -r build/demodal-v3.zip $files

    mv manifest.json.bak manifest.json
    ;;

  *)
    echo Invalid argument $1
    ;;
esac
Download .txt
gitextract_jysvujgv/

├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── _locales/
│   ├── en/
│   │   └── messages.json
│   └── ru/
│       └── messages.json
├── background/
│   ├── background.html
│   └── background.js
├── common/
│   ├── common.css
│   └── common.js
├── content/
│   └── content.js
├── definitions/
│   ├── consent.json
│   ├── donate.json
│   ├── email.json
│   ├── message.json
│   ├── offer.json
│   ├── paywall.json
│   └── signup.json
├── inject/
│   └── inject.js
├── manifest-v2.json
├── manifest-v3.json
├── options/
│   ├── options.css
│   ├── options.html
│   └── options.js
├── package.json
├── popup/
│   ├── popup.css
│   ├── popup.html
│   └── popup.js
└── run
Download .txt
SYMBOL INDEX (39 symbols across 5 files)

FILE: background/background.js
  function loadDefinitions (line 11) | async function loadDefinitions() {
  method getDefinitions (line 33) | async getDefinitions() {
  method setBadge (line 64) | setBadge(text) {
  method log (line 71) | log(...args) {
  method error (line 76) | error(error) {

FILE: common/common.js
  method isObject (line 35) | isObject(object) {
  method arrayify (line 39) | arrayify(item) {
  method debounce (line 43) | debounce(func, wait) {
  method i18n (line 59) | i18n() {
  method capitalize (line 67) | capitalize(string) {
  method removeChildren (line 71) | removeChildren(element) {
  method el (line 77) | el(name) {
  method getActiveTab (line 81) | getActiveTab() {
  method tokenify (line 89) | async tokenify(string) {
  method transformDefinitions (line 156) | async transformDefinitions(definitionsByType) {
  method globToRegExp (line 263) | globToRegExp(glob) {
  method inject (line 289) | inject(func, ...args) {
  method remove (line 322) | remove() {
  method removeParent (line 327) | removeParent(level = 1) {
  method removeIf (line 342) | removeIf(...args) {
  method addClass (line 355) | addClass(...args) {
  method removeClass (line 360) | removeClass(...args) {
  method addStyle (line 369) | addStyle(...args) {
  method removeStyle (line 374) | removeStyle() {
  method click (line 379) | click() {
  method call (line 384) | call(...args) {
  method $ (line 392) | $(selector) {
  method defined (line 399) | async defined(...args) {
  method sleep (line 406) | sleep(ms = 0) {
  method call (line 418) | call(func, ...args) {
  method call (line 430) | call(func, ...args) {

FILE: content/content.js
  method getBlockedModals (line 12) | getBlockedModals() {
  method getUrl (line 16) | getUrl() {
  method reload (line 20) | reload() {
  method isAllowed (line 24) | async isAllowed(url = window.location.href) {
  function log (line 39) | function log(...messages) {

FILE: inject/inject.js
  method defined (line 20) | defined(chain) {
  method call (line 23) | call(chain, ...args) {

FILE: popup/popup.js
  function renderTotals (line 16) | function renderTotals(prefix, totals) {
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (61K chars).
[
  {
    "path": ".editorconfig",
    "chars": 207,
    "preview": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_"
  },
  {
    "path": ".eslintignore",
    "chars": 22,
    "preview": "**/lib/*\nnode_modules\n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 327,
    "preview": "module.exports = {\n  root: true,\n  env: {\n    browser: true,\n    node: true,\n  },\n  parserOptions: {\n    parser: 'babel-"
  },
  {
    "path": ".gitignore",
    "chars": 114,
    "preview": "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",
    "chars": 70,
    "preview": "{\n  \"semi\": false,\n  \"arrowParens\": \"always\",\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2021 Elbert Alias\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 4940,
    "preview": "\n# Demodal\n\nDemodal is a browser extension that automatically removes content blocking modals including paywalls, \ndisco"
  },
  {
    "path": "_locales/en/messages.json",
    "chars": 1808,
    "preview": "{\n  \"modalType\": { \"message\": \"Type\" },\n  \"blockedModals\": { \"message\": \"Blocked\" },\n  \"blockedModalsPage\": { \"message\":"
  },
  {
    "path": "_locales/ru/messages.json",
    "chars": 2127,
    "preview": "{\n  \"modalType\": { \"message\": \"Тип\" },\n  \"blockedModals\": { \"message\": \"Заблокировано\" },\n  \"blockedModalsPage\": { \"mess"
  },
  {
    "path": "background/background.html",
    "chars": 203,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n\n\t\t<title></title>\n  </head>\n  <body>\n    <script src=\"../com"
  },
  {
    "path": "background/background.js",
    "chars": 2837,
    "preview": "/* globals chrome, importScripts, Common */\n\nif (typeof importScripts !== 'undefined') {\n  importScripts(chrome.runtime."
  },
  {
    "path": "common/common.css",
    "chars": 2864,
    "preview": ":root {\n  --color-primary: #667aff;\n  --color-primary-dark: #4755b3;\n  --color-primary-light: #f1f3ff;\n  --color-heading"
  },
  {
    "path": "common/common.js",
    "chars": 10652,
    "preview": "/* globals chrome */\n\n// Manifest v2 polyfill\nif (chrome.runtime.getManifest().manifest_version === 2) {\n  chrome.action"
  },
  {
    "path": "content/content.js",
    "chars": 3816,
    "preview": "/* eslint-env browser */\n/* globals chrome, Common */\n\nconst { $, debounce, Background, Actions, Functions } = Common\n\nc"
  },
  {
    "path": "definitions/consent.json",
    "chars": 2125,
    "preview": "{\n  \"*\": {\n    \"#cookie-law-info-bar\": \"remove\",\n    \"#moove_gdpr_cookie_info_bar\": \"remove\",\n    \"if defined(BorlabsCoo"
  },
  {
    "path": "definitions/donate.json",
    "chars": 186,
    "preview": "{\n  \"*.theguardian.com\": {\n    \".site-message--banner\": \"remove\",\n    \"#bottom-banner\": \"removeIf 'Support the Guardian'"
  },
  {
    "path": "definitions/email.json",
    "chars": 725,
    "preview": "{\n  \"*\": {\n    \"if $(.elementor-popup-modal)\": {\n      \".elementor-popup-modal\": \"remove\",\n      \"body\": \"removeClass di"
  },
  {
    "path": "definitions/message.json",
    "chars": 353,
    "preview": "{\n  \"*\": {\n    \"if $([id^='sp_message_container_'])\": {\n      \"[id^='sp_message_container_']\": \"remove\",\n      \"html\": \""
  },
  {
    "path": "definitions/offer.json",
    "chars": 137,
    "preview": "{\n  \"*.forbes.com\": {\n    \"[external-event='offer-close-modal']\": \"click\"\n  },\n  \"*.theconversation.com\": {\n    \".promo\""
  },
  {
    "path": "definitions/paywall.json",
    "chars": 1443,
    "preview": "{\n  \"*.nytimes.com\": {\n    \"if $(#gateway-content)\": {\n      \"#gateway-content\": \"remove\",\n      \"#app > div > div\": \"re"
  },
  {
    "path": "definitions/signup.json",
    "chars": 1895,
    "preview": "{\n  \"*\": {\n    \".wbounce-modal\": \"removeIf 'newsletter'\",\n    \".leadinModal\": \"remove\",\n    \".wppopups-whole\": \"removeIf"
  },
  {
    "path": "inject/inject.js",
    "chars": 1079,
    "preview": "/* eslint-env browser */\n\n;(function () {\n  try {\n    const chainToProp = (chain) => {\n      return chain\n        .split"
  },
  {
    "path": "manifest-v2.json",
    "chars": 900,
    "preview": "{\n  \"name\": \"Demodal - Block modals and overlays\",\n  \"description\": \"Demodal automatically removes content blocking moda"
  },
  {
    "path": "manifest-v3.json",
    "chars": 849,
    "preview": "{\n  \"name\": \"Demodal - Block modals and overlays\",\n  \"description\": \"Demodal automatically removes content blocking moda"
  },
  {
    "path": "options/options.css",
    "chars": 78,
    "preview": ".container {\n  max-width: 600px;\n  margin-left: auto;\n  margin-right: auto;\n}\n"
  },
  {
    "path": "options/options.html",
    "chars": 577,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n\n\t\t<title data-i18n=\"options\"></title>\n\n    <link rel=\"styles"
  },
  {
    "path": "options/options.js",
    "chars": 1042,
    "preview": "/* globals chrome, Common */\n\nconst { $, i18n, el, capitalize, modalTypes } = Common\n\ni18n()\n\n//\n;(async () => {\n  const"
  },
  {
    "path": "package.json",
    "chars": 343,
    "preview": "{\n  \"devDependencies\": {\n    \"@nuxtjs/eslint-config\": \"^3.1.0\",\n    \"@nuxtjs/eslint-module\": \"^2.0.0\",\n    \"babel-eslint"
  },
  {
    "path": "popup/popup.css",
    "chars": 487,
    "preview": "body {\n  width: 500px;\n}\n\n.header {\n  background-color: var(--color-primary);\n  display: flex;\n  align-items: center;\n  "
  },
  {
    "path": "popup/popup.html",
    "chars": 4732,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n\n    <title></title>\n\n    <link rel=\"stylesheet\" href=\"../com"
  },
  {
    "path": "popup/popup.js",
    "chars": 5849,
    "preview": "/* globals chrome, Common */\n\nconst {\n  $,\n  $$,\n  i18n,\n  capitalize,\n  removeChildren,\n  el,\n  debounce,\n  modalTypes,"
  },
  {
    "path": "run",
    "chars": 939,
    "preview": "#!/bin/bash\n\ncase $1 in\n  manifest)\n    if [[ -f manifest-$2.json ]]; then\n      cat manifest-$2.json > manifest.json\n\n "
  }
]

About this extraction

This page contains the full source code of the AliasIO/demodal GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (53.5 KB), approximately 14.9k tokens, and a symbol index with 39 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!