Repository: darkreader/darkreader
Pull request #2496 (2020-05-12 09:31:14)

Added detexify.kirelabs.org
Images was barely visible on http://detexify.kirelabs.org/classify.html


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index bf815710..961f82d7 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -39,7 +39,7 @@ module.exports = {
     },
     overrides: [
         {
-            files: ['tasks/**/*.js'],
+            files: ['tasks/**/*.js', 'tests/**/*.js'],
             rules: {
                 '@typescript-eslint/no-var-requires': 'off',
             },
diff --git a/package-lock.json b/package-lock.json
index aa97441e..2cc08b67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1775,7 +1775,7 @@
     },
     "array-equal": {
       "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
+      "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
       "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=",
       "dev": true
     },
@@ -6147,9 +6147,9 @@
       }
     },
     "malevic": {
-      "version": "0.18.2",
-      "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.18.2.tgz",
-      "integrity": "sha512-uB5Y5GKBfytyGNW9IDs/twyGQJeigQJsOZRdR+mJ2aEUDdQjtsXsMBvJJHFx7rsIINSm4QAlzLGeENST4BbJ6A=="
+      "version": "0.18.3",
+      "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.18.3.tgz",
+      "integrity": "sha512-RswIMGEfyFOjLK9JW82AWMcK9PLoEi27oZ7vWxVliBPnIYe9DBVcGEGlDoFd5A36YhVp/3Ci/aNasS6s4XYSLw=="
     },
     "map-cache": {
       "version": "0.2.2",
diff --git a/package.json b/package.json
index 16e2d56a..0506a58a 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,9 @@
   "description": "Dark theme for every website",
   "scripts": {
     "api": "node -e \"require('./tasks/bundle-api')()\"",
+    "benchmark-server": "node tests/benchmark-server/index.js",
     "build": "node tasks/release.js",
-    "code-style": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" \"tasks/**/*.js\" \"tests/**/*.ts\" \"index.d.ts\"",
+    "code-style": "eslint --fix \"src/**/*.ts\" \"src/**/*.tsx\" \"tasks/**/*.js\" \"tests/**/*.ts\" \"tests/**/*.js\" \"index.d.ts\"",
     "debug": "node tasks/debug.js",
     "prepublishOnly": "npm test && npm run api",
     "release": "npm test && node tasks/release.js",
@@ -28,7 +29,7 @@
     "url": "https://opencollective.com/darkreader"
   },
   "dependencies": {
-    "malevic": "0.18.2"
+    "malevic": "0.18.3"
   },
   "devDependencies": {
     "@rollup/plugin-commonjs": "11.1.0",
diff --git a/src/background/user-storage.ts b/src/background/user-storage.ts
index 94e2b7f3..7c4139a2 100644
--- a/src/background/user-storage.ts
+++ b/src/background/user-storage.ts
@@ -93,6 +93,7 @@ export default class UserStorage {
                 let isOK = false;
                 try {
                     isURLMatched('https://google.com/', pattern);
+                    isURLMatched('[::1]:1337', pattern);
                     isOK = true;
                 } catch (err) {
                     console.warn(`Pattern "${pattern}" excluded`);
diff --git a/src/config/dark-sites.config b/src/config/dark-sites.config
index fd128bb7..374b203f 100644
--- a/src/config/dark-sites.config
+++ b/src/config/dark-sites.config
@@ -97,7 +97,6 @@ energized.pro
 enlightenment.org
 entity.works
 envs.net
-epicgames.com
 erai-raws.info
 esportal.com
 eternal.abimon.org
@@ -122,6 +121,7 @@ gidonline.in
 giphy.com
 gitkraken.com
 glowing-bear.org
+gogalaxy.com
 gogoanime.se
 graydon2.dreamwidth.org
 grc.arikado.ru
@@ -206,12 +206,14 @@ rawg.io
 razorsecure.com
 realgamernewz.com
 redbox.com
+remixpacks.ru
 runicgames.com
 saidit.net
 sanctum.geek.nz/arabesque
 sandervanderburg.blogspot.com
 sapphdavis.com
 sapphdavis.me
+saznajnovo.com
 secrethitler.io
 serebii.net
 sheet.host
@@ -236,7 +238,6 @@ steamstat.us
 steamtimeidler.com
 store.steampowered.com
 streamelements.com
-streamlabs.com
 sufficientvelocity.com
 supinic.com
 svtplay.se
diff --git a/src/config/dynamic-theme-fixes.config b/src/config/dynamic-theme-fixes.config
index e7262a67..1135a7b0 100644
--- a/src/config/dynamic-theme-fixes.config
+++ b/src/config/dynamic-theme-fixes.config
@@ -292,6 +292,15 @@ CSS
 
 ================================
 
+atlassian.net
+
+CSS
+#jira-issue-header {
+    background-color: transparent !important;
+}
+
+================================
+
 avito.ru
 
 INVERT
@@ -477,6 +486,43 @@ img[src$="dark_create_class_arrow.svg"]
 
 ================================
 
+cloud.databricks.com
+*.azuredatabricks.net
+
+INVERT
+.figure
+
+CSS
+text {
+  fill: white;
+}
+
+.cm-string {
+  color: rgb(132, 179, 235) !important;
+}
+
+.cm-keyword {
+  color: rgb(232, 121, 172) !important;
+}
+
+.cm-variable-2 {
+  color: rgb(97, 215, 255) !important;
+}
+
+li.CodeMirror-hint {
+ font-family: Source Code Pro, Menlo, monospace;
+}
+
+span[role="presentation"] {
+  color: white !important;
+}
+
+.CodeMirror-cursor {
+  border-left-color: white !important;
+}
+
+================================
+
 code.qt.io
 
 INVERT
@@ -1028,7 +1074,7 @@ div[aria-label="Change Position"] .ha302278,
     fill: none !important;
 }
 .s1i5eluu {
-    background-color: var(--primary-button-background);
+    background-color: var(--primary-button-background) !important;
 }
 .q66pz984 {
     color: var(--accent);
@@ -1038,7 +1084,7 @@ div[aria-label="Change Position"] .ha302278,
 }
 div[role="button"] > i.hu5pjgll, a[role="link"] > i.hu5pjgll,
 ._3w97, .q2y6ezfg, .mkbloq8g {
-    filter: brightness(1000%) !important;
+    filter: var(--filter-secondary-icon) !important;
 }
 ._2yua, ._3pas {
     background-color: #888 !important;
@@ -1172,6 +1218,38 @@ CSS
 
 ================================
 
+gazetawroclawska.pl
+
+INVERT
+.atomsNavigationFooterLegal
+.componentsSiteHeader
+.atomsNavigationFooterAccordion
+.atomsNavigationFooterSocialmedia__link
+.atomsNavigationFooterSocialmedia__button
+.atomsArticlePollVoting__pulseDescription
+.atomsArticlePollVoting__form
+.atomsArticlePollVoting__voteTitle
+.componentsArticleGallery__captionDescription
+.componentsPromotionsVideo__title
+.layoutsGrid
+article .atomsArticleTile__tileImg
+.layoutsGrid--last .atomsArticleTile__tileImg
+.componentsArticleGallery__img
+.componentsArticleGallery__thumbnail
+.atomsArticleFootTools__button
+.componentsPromotionsOffers__fakeImg
+.atomsNavigationFooterLinks__logoImg
+.ytp-cued-thumbnail-overlay
+.componentsRecommendationsSelected__photo
+.componentsPromotionsSubjectCategoryPromoted__image
+p.nieprzeczytane
+p.przeczytane
+article iframe
+.atomsPromotionsQuiz__imageWrapper
+.atomsPromotionsLink__imageWrapper
+
+================================
+
 geizhals.at
 geizhals.de
 
@@ -1182,6 +1260,20 @@ img.listview__image, img.galleryview__image {
 
 ================================
 
+geogebra.org
+
+INVERT
+.icon-m
+.elemText canvas
+.gwt-Image
+.gwt-StackPanelItem img
+.GeoGebraMenuImage.menuImg
+.menuImg
+.buttonContent.stylebarButton
+.EuclidianPanel > canvas
+
+================================
+
 get.todoist.help
 
 CSS
@@ -2063,6 +2155,38 @@ CSS
 
 ================================
 
+naszemiasto.pl
+
+INVERT
+.componentsNavigationNavbar
+.atomsNavigationFooterLegal
+.componentsSiteHeader
+.atomsNavigationFooterAccordion
+.atomsNavigationFooterSocialmedia__link
+.atomsNavigationFooterSocialmedia__button
+.atomsArticlePollVoting__pulseDescription
+.atomsArticlePollVoting__form
+.atomsArticlePollVoting__voteTitle
+.componentsArticleGallery__captionDescription
+.componentsPromotionsVideo__title
+.layoutsGrid
+aside .atomsArticleTile__tileImg
+.componentsArticleGallery__img
+.componentsArticleGallery__thumbnail
+.atomsArticleFootTools__button
+.componentsPromotionsOffers__fakeImg
+.atomsNavigationFooterLinks__logoImg
+.ytp-cued-thumbnail-overlay
+.componentsPromotionsSubjectCategoryPromoted__image
+.componentsRecommendationsSelected__photo
+p.nieprzeczytane
+p.przeczytane
+article iframe
+.atomsPromotionsQuiz__imageWrapper
+.atomsPromotionsLink__imageWrapper
+
+================================
+
 natemat.pl
 
 INVERT
@@ -2090,6 +2214,13 @@ INVERT
 
 ================================
 
+nexojornal.com.br
+
+INVERT
+.g-aiImg
+
+================================
+
 ngrok.com
 
 INVERT
@@ -2216,6 +2347,13 @@ CSS
 
 ================================
 
+omnicalculator.com
+
+INVERT
+.GenericText
+
+================================
+
 onet.pl
 
 INVERT
@@ -3265,6 +3403,31 @@ INVERT
 
 ================================
 
+treccani.it
+*.treccani.it
+
+CSS
+.treccani_portal .wrapper, .treccani_portal body {
+    background-color: ${white} !important;
+}
+.module-search-preview-more_results .module-search-preview-more_results-related_container {
+    background-color: ${lightgray} !important;
+}
+.module-widget .widget-bg {
+    background-color: ${lightgray} !important;
+}
+.article_template-bg-text-medium .block .block-content {
+    background-color: ${lightgray} !important;
+}
+.module-footer .footer-colophon {
+    background-color: ${lightgray} !important;
+}
+#nav-icon3 span {
+    background-color: ${black} !important;
+}
+
+================================
+
 tw.appledaily.com
 
 INVERT
@@ -3450,6 +3613,21 @@ body {
 
 ================================
 
+vrt.be/vrtnws
+
+INVERT
+.vrt-site-header__home>svg
+
+CSS
+article:hover::before {
+    background-color: ${white} !important;
+}
+.vrt-site-header__container::before {
+    background-color: transparent !important;
+}
+
+================================
+
 w3.org
 
 INVERT
diff --git a/src/config/inversion-fixes.config b/src/config/inversion-fixes.config
index 90b0c152..f4ea78b3 100644
--- a/src/config/inversion-fixes.config
+++ b/src/config/inversion-fixes.config
@@ -297,6 +297,13 @@ NO INVERT
 
 ================================
 
+detexify.kirelabs.org
+
+INVERT
+.symbol img
+
+================================
+
 devdocs.io
 
 NO INVERT
@@ -1457,6 +1464,9 @@ outlook.live.com
 
 INVERT
 #O365_NavHeader
+span.ms-Pivot-text
+span.ms-Pivot-icon
+.ms-FocusZone.ms-CommandBar
 
 ================================
 
diff --git a/src/generators/utils/format.ts b/src/generators/utils/format.ts
index c90ad0b3..c9e9cb6a 100644
--- a/src/generators/utils/format.ts
+++ b/src/generators/utils/format.ts
@@ -1,3 +1,5 @@
+import {push} from '../../utils/array';
+
 interface SiteFix {
     url: string[];
     [prop: string]: any;
@@ -14,7 +16,7 @@ export function formatSitesFixesConfig(fixes: SiteFix[], options: SitesFixesForm
     const lines: string[] = [];
 
     fixes.forEach((fix, i) => {
-        lines.push(...fix.url);
+        push(lines, fix.url);
         options.props.forEach((prop) => {
             const command = options.getPropCommandName(prop);
             const value = fix[prop];
@@ -30,7 +32,7 @@ export function formatSitesFixesConfig(fixes: SiteFix[], options: SitesFixesForm
         });
         if (i < fixes.length - 1) {
             lines.push('');
-            lines.push(Array.from({length: 32}).fill('=').join(''));
+            lines.push('='.repeat(32));
             lines.push('');
         }
     });
diff --git a/src/inject/dynamic-theme/css-rules.ts b/src/inject/dynamic-theme/css-rules.ts
index 13a9e838..b8520f19 100644
--- a/src/inject/dynamic-theme/css-rules.ts
+++ b/src/inject/dynamic-theme/css-rules.ts
@@ -1,30 +1,30 @@
+import {forEach} from '../../utils/array';
 import {parseURL, getAbsoluteURL} from './url';
 import {logWarn} from '../utils/log';
 
 export function iterateCSSRules(rules: CSSRuleList, iterate: (rule: CSSStyleRule) => void) {
-    Array.from(rules)
-        .forEach((rule) => {
-            if (rule instanceof CSSMediaRule) {
-                const media = Array.from(rule.media);
-                if (media.includes('screen') || media.includes('all') || !(media.includes('print') || media.includes('speech'))) {
-                    iterateCSSRules(rule.cssRules, iterate);
-                }
-            } else if (rule instanceof CSSStyleRule) {
-                iterate(rule);
-            } else if (rule instanceof CSSImportRule) {
-                try {
-                    iterateCSSRules(rule.styleSheet.cssRules, iterate);
-                } catch (err) {
-                    logWarn(err);
-                }
-            } else {
-                logWarn(`CSSRule type not supported`, rule);
+    forEach(rules, (rule) => {
+        if (rule instanceof CSSMediaRule) {
+            const media = Array.from(rule.media);
+            if (media.includes('screen') || media.includes('all') || !(media.includes('print') || media.includes('speech'))) {
+                iterateCSSRules(rule.cssRules, iterate);
             }
-        });
+        } else if (rule instanceof CSSStyleRule) {
+            iterate(rule);
+        } else if (rule instanceof CSSImportRule) {
+            try {
+                iterateCSSRules(rule.styleSheet.cssRules, iterate);
+            } catch (err) {
+                logWarn(err);
+            }
+        } else {
+            logWarn(`CSSRule type not supported`, rule);
+        }
+    });
 }
 
 export function iterateCSSDeclarations(style: CSSStyleDeclaration, iterate: (property: string, value: string) => void) {
-    Array.from(style).forEach((property) => {
+    forEach(style, (property) => {
         const value = style.getPropertyValue(property).trim();
         if (!value) {
             return;
diff --git a/src/inject/dynamic-theme/index.ts b/src/inject/dynamic-theme/index.ts
index 5421191d..05219f6b 100644
--- a/src/inject/dynamic-theme/index.ts
+++ b/src/inject/dynamic-theme/index.ts
@@ -4,6 +4,7 @@ import {changeMetaThemeColorWhenAvailable, restoreMetaThemeColor} from './meta-t
 import {getModifiedUserAgentStyle, getModifiedFallbackStyle, cleanModificationCache, parseColorWithCache} from './modify-css';
 import {manageStyle, shouldManageStyle, STYLE_SELECTOR, StyleManager} from './style-manager';
 import {watchForStyleChanges, stopWatchingForStyleChanges} from './watch';
+import {forEach, push, toArray} from '../../utils/array';
 import {removeNode, watchForNodePosition, iterateShadowNodes} from '../utils/dom';
 import {logWarn} from '../utils/log';
 import {throttle} from '../utils/throttle';
@@ -39,7 +40,7 @@ function setupStylePositionWatcher(node: Node, alias: string) {
 }
 
 function stopStylePositionWatchers() {
-    Array.from(stylePositionWatchers.values()).forEach((watcher) => watcher.stop());
+    forEach(stylePositionWatchers.values(), (watcher) => watcher.stop());
     stylePositionWatchers.clear();
 }
 
@@ -123,15 +124,15 @@ function createDynamicStyleOverrides() {
 
     updateVariables(getElementCSSVariables(document.documentElement));
 
-    const allStyles = Array.from(document.querySelectorAll(STYLE_SELECTOR)) as (HTMLLinkElement | HTMLStyleElement)[];
+    const allStyles = toArray(document.querySelectorAll(STYLE_SELECTOR)) as (HTMLLinkElement | HTMLStyleElement)[];
     iterateShadowNodes(document.documentElement, (node) => {
         const shadowStyles = node.shadowRoot.querySelectorAll(STYLE_SELECTOR);
         if (shadowStyles.length > 0) {
-            allStyles.push(...Array.from(shadowStyles as NodeListOf<HTMLLinkElement | HTMLStyleElement>));
+            push(allStyles, shadowStyles);
         }
     });
 
-    const newManagers = Array.from<HTMLLinkElement | HTMLStyleElement>(allStyles)
+    const newManagers = toArray<HTMLLinkElement | HTMLStyleElement>(allStyles)
         .filter((style) => !styleManagers.has(style) && shouldManageStyle(style))
         .map((style) => createManager(style));
     const newVariables = newManagers
@@ -153,12 +154,12 @@ function createDynamicStyleOverrides() {
     }
     newManagers.forEach((manager) => manager.watch());
 
-    const inlineStyleElements = Array.from(document.querySelectorAll(INLINE_STYLE_SELECTOR));
+    const inlineStyleElements = toArray(document.querySelectorAll(INLINE_STYLE_SELECTOR));
     iterateShadowNodes(document.documentElement, (node) => {
         const elements = node.shadowRoot.querySelectorAll(INLINE_STYLE_SELECTOR);
         if (elements.length > 0) {
             createShadowStaticStyleOverrides(node.shadowRoot);
-            inlineStyleElements.push(...Array.from(elements as NodeListOf<HTMLLinkElement | HTMLStyleElement>));
+            push(inlineStyleElements, elements);
         }
     });
     inlineStyleElements.forEach((el) => overrideInlineStyle(el as HTMLElement, filter, fixes.ignoreInlineStyle));
@@ -378,8 +379,8 @@ export function removeDynamicTheme() {
         removeNode(root.querySelector('.darkreader--inline'));
     });
     shadowRootsWithOverrides.clear();
-    Array.from(styleManagers.keys()).forEach((el) => removeManager(el));
-    Array.from(document.querySelectorAll('.darkreader')).forEach(removeNode);
+    forEach(styleManagers.keys(), (el) => removeManager(el));
+    document.querySelectorAll('.darkreader').forEach(removeNode);
 }
 
 export function cleanDynamicThemeCache() {
diff --git a/src/inject/dynamic-theme/inline-style.ts b/src/inject/dynamic-theme/inline-style.ts
index 0ff66696..0345f092 100644
--- a/src/inject/dynamic-theme/inline-style.ts
+++ b/src/inject/dynamic-theme/inline-style.ts
@@ -1,3 +1,4 @@
+import {forEach, push} from '../../utils/array';
 import {iterateShadowNodes} from '../utils/dom';
 import {iterateCSSDeclarations} from './css-rules';
 import {getModifiableCSSDeclaration} from './modify-css';
@@ -102,14 +103,14 @@ export function getInlineOverrideStyle() {
     }).join('\n');
 }
 
-function expand(nodes: Node[], selector: string) {
+function expand(nodes: ArrayLike<Node>, selector: string) {
     const results: Node[] = [];
-    nodes.forEach((n) => {
+    forEach(nodes, (n) => {
         if (n instanceof Element) {
             if (n.matches(selector)) {
                 results.push(n);
             }
-            results.push(...Array.from(n.querySelectorAll(selector)));
+            push(results, n.querySelectorAll(selector));
         }
     });
     return results;
@@ -137,7 +138,7 @@ export function deepWatchForInlineStyles(
     }
     const observer = new MutationObserver((mutations) => {
         mutations.forEach((m) => {
-            const createdInlineStyles = expand(Array.from(m.addedNodes), INLINE_STYLE_SELECTOR);
+            const createdInlineStyles = expand(m.addedNodes, INLINE_STYLE_SELECTOR);
             if (createdInlineStyles.length > 0) {
                 createdInlineStyles.forEach((el: HTMLElement) => elementStyleDidChange(el));
             }
@@ -186,7 +187,8 @@ function getInlineStyleCacheKey(el: HTMLElement, theme: FilterConfig) {
 }
 
 function shouldIgnoreInlineStyle(element: HTMLElement, selectors: string[]) {
-    for (const ingnoredSelector of selectors) {
+    for (let i = 0; i < selectors.length; i++) {
+        const ingnoredSelector = selectors[i];
         if (element.matches(ingnoredSelector)) {
             return true;
         }
@@ -274,7 +276,7 @@ export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, i
         setCustomProp('fill', 'color', element.style.getPropertyValue('fill'));
     }
 
-    Array.from(unsetProps).forEach((cssProp) => {
+    forEach(unsetProps, (cssProp) => {
         const {store, dataAttr} = overrides[cssProp];
         store.delete(element);
         element.removeAttribute(dataAttr);
diff --git a/src/inject/dynamic-theme/meta-theme-color.ts b/src/inject/dynamic-theme/meta-theme-color.ts
index 4aaf5aad..963c7b8c 100644
--- a/src/inject/dynamic-theme/meta-theme-color.ts
+++ b/src/inject/dynamic-theme/meta-theme-color.ts
@@ -27,8 +27,10 @@ export function changeMetaThemeColorWhenAvailable(theme: FilterConfig) {
             observer.disconnect();
         }
         observer = new MutationObserver((mutations) => {
-            loop: for (const m of mutations) {
-                for (const node of Array.from(m.addedNodes)) {
+            loop: for (let i = 0; i < mutations.length; i++) {
+                const {addedNodes} = mutations[i];
+                for (let j = 0; j < addedNodes.length; j++) {
+                    const node = addedNodes[j];
                     if (node instanceof HTMLMetaElement && node.name === metaThemeColorName) {
                         observer.disconnect();
                         observer = null;
diff --git a/src/inject/dynamic-theme/watch.ts b/src/inject/dynamic-theme/watch.ts
index c3172b5f..255bffdf 100644
--- a/src/inject/dynamic-theme/watch.ts
+++ b/src/inject/dynamic-theme/watch.ts
@@ -1,4 +1,5 @@
 import {iterateShadowNodes} from '../utils/dom';
+import {forEach, push} from '../../utils/array';
 import {isDefinedSelectorSupported} from '../../utils/platform';
 import {shouldManageStyle, STYLE_SELECTOR} from './style-manager';
 
@@ -13,18 +14,19 @@ interface ChangedStyles {
 
 function getAllManageableStyles(nodes: Iterable<Node> | ArrayLike<Node>) {
     const results: (HTMLLinkElement | HTMLStyleElement)[] = [];
-    Array.from(nodes).forEach((node) => {
+    forEach(nodes, (node) => {
         if (node instanceof Element) {
             if (shouldManageStyle(node)) {
                 results.push(node as HTMLLinkElement | HTMLStyleElement);
             }
         }
         if (node instanceof Element || node instanceof ShadowRoot) {
-            results.push(
-                ...Array.from<HTMLLinkElement | HTMLStyleElement>(
-                    node.querySelectorAll(STYLE_SELECTOR)
-                ).filter(shouldManageStyle)
-            );
+            node.querySelectorAll(STYLE_SELECTOR)
+                .forEach((style: HTMLLinkElement | HTMLStyleElement) => {
+                    if (shouldManageStyle(style)) {
+                        results.push(style);
+                    }
+                });
         }
     });
     return results;
@@ -124,7 +126,7 @@ export function watchForStyleChanges(update: (styles: ChangedStyles) => void) {
             iterateShadowNodes(n, (host) => {
                 const shadowStyles = getAllManageableStyles(host.shadowRoot.children);
                 if (shadowStyles.length > 0) {
-                    styleAdditions.push(...shadowStyles);
+                    push(styleAdditions, shadowStyles);
                 }
             });
         });
@@ -132,7 +134,7 @@ export function watchForStyleChanges(update: (styles: ChangedStyles) => void) {
             iterateShadowNodes(n, (host) => {
                 const shadowStyles = getAllManageableStyles(host.shadowRoot.children);
                 if (shadowStyles.length > 0) {
-                    styleDeletions.push(...shadowStyles);
+                    push(styleDeletions, shadowStyles);
                 }
             });
         });
diff --git a/src/ui/controls/color-dropdown/index.tsx b/src/ui/controls/color-dropdown/index.tsx
index e1b57deb..cff16d6c 100644
--- a/src/ui/controls/color-dropdown/index.tsx
+++ b/src/ui/controls/color-dropdown/index.tsx
@@ -51,7 +51,7 @@ export default function ColorDropDown(props: ColorDropDownProps) {
 
     let isPickerVisible: boolean;
 
-    try { 
+    try {
         parse(props.value);
         isPickerVisible = true;
     } catch {
diff --git a/src/ui/controls/color-picker/index.tsx b/src/ui/controls/color-picker/index.tsx
index b443ad28..a4d9fb0d 100644
--- a/src/ui/controls/color-picker/index.tsx
+++ b/src/ui/controls/color-picker/index.tsx
@@ -12,7 +12,7 @@ interface ColorPickerProps {
 const DEFAULT_COLOR = '#000000';
 
 function isValidColor(color: string) {
-    try { 
+    try {
         parse(color);
         return true;
     } catch {
diff --git a/src/ui/controls/index.ts b/src/ui/controls/index.ts
index da49fea4..def8cf76 100644
--- a/src/ui/controls/index.ts
+++ b/src/ui/controls/index.ts
@@ -3,8 +3,10 @@ import CheckBox from './checkbox';
 import ColorDropDown from './color-dropdown';
 import ColorPicker from './color-picker';
 import DropDown from './dropdown';
+import MessageBox from './message-box';
 import MultiSwitch from './multi-switch';
 import NavButton from './nav-button';
+import Overlay from './overlay';
 import ResetButton from './reset-button';
 import Select from './select';
 import Shortcut from './shortcut';
@@ -22,8 +24,10 @@ export {
     ColorDropDown,
     ColorPicker,
     DropDown,
+    MessageBox,
     MultiSwitch,
     NavButton,
+    Overlay,
     ResetButton,
     Select,
     Shortcut,
diff --git a/src/ui/controls/message-box/index.tsx b/src/ui/controls/message-box/index.tsx
new file mode 100644
index 00000000..2493f19b
--- /dev/null
+++ b/src/ui/controls/message-box/index.tsx
@@ -0,0 +1,30 @@
+import {m} from 'malevic';
+import Button from '../button';
+import Overlay from '../overlay';
+
+interface MessageBoxProps {
+    caption: string;
+    onOK?: () => void;
+    onCancel?: () => void;
+    portalKey?: any;
+}
+
+export default function MessageBox(props: MessageBoxProps) {
+    return (
+        <Overlay.Portal key={props.portalKey} onOuterClick={props.onCancel}>
+            <div class="message-box">
+                <label class="message-box__caption">
+                    {props.caption}
+                </label>
+                <div class="message-box__buttons">
+                    <Button class="message-box__button message-box__button-ok" onclick={props.onOK}>
+                        OK
+                    </Button>
+                    <Button class="message-box__button message-box__button-cancel" onclick={props.onCancel}>
+                        Cancel
+                    </Button>
+                </div>
+            </div>
+        </Overlay.Portal>
+    );
+}
diff --git a/src/ui/controls/message-box/style.less b/src/ui/controls/message-box/style.less
new file mode 100644
index 00000000..35caa248
--- /dev/null
+++ b/src/ui/controls/message-box/style.less
@@ -0,0 +1,33 @@
+@import "../../theme.less";
+
+.message-box {
+    background-color: @color-back;
+    border-bottom: @size-border solid @color-border;
+    border-top: @size-border solid @color-border;
+    display: flex;
+    flex-direction: column;
+    padding: @indent-large;
+
+    &__caption {
+        text-align: center;
+    }
+
+    &__buttons {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        margin-top: @indent-small;
+    }
+
+    &__button {
+        min-width: 4rem;
+
+        &:not(:first-child) {
+            margin-left: @indent-small;
+        }
+    }
+
+    &__button-ok {
+        border-color: gray;
+    }
+}
diff --git a/src/ui/controls/overlay/index.ts b/src/ui/controls/overlay/index.ts
new file mode 100644
index 00000000..ecb51cf4
--- /dev/null
+++ b/src/ui/controls/overlay/index.ts
@@ -0,0 +1,64 @@
+import {getContext, render} from 'malevic/dom';
+import {isStringifying} from 'malevic/string';
+
+const DEFAULT_OVERLAY_KEY = Symbol();
+const overlayNodes = new Map<any, HTMLElement>();
+const clickListeners = new WeakMap<HTMLElement, () => void>();
+
+function getOverlayDOMNode(key: any) {
+    if (key == null) {
+        key = DEFAULT_OVERLAY_KEY;
+    }
+
+    if (!overlayNodes.has(key)) {
+        const node = document.createElement('div');
+        node.classList.add('overlay');
+        node.addEventListener('click', (e) => {
+            if (clickListeners.has(node) && e.currentTarget === node) {
+                const listener = clickListeners.get(node);
+                listener();
+            }
+        });
+        overlayNodes.set(key, node);
+    }
+    return overlayNodes.get(key);
+}
+
+interface OverlayProps {
+    key?: any;
+}
+
+function Overlay(props: OverlayProps) {
+    if (isStringifying()) {
+        return null;
+    }
+    return getOverlayDOMNode(props.key);
+}
+
+interface OverlayPortalProps {
+    key?: any;
+    onOuterClick?: () => void;
+}
+
+function Portal(props: OverlayPortalProps, ...content: Malevic.Child[]) {
+    const context = getContext();
+
+    context.onRender(() => {
+        const node = getOverlayDOMNode(props.key);
+        if (props.onOuterClick) {
+            clickListeners.set(node, props.onOuterClick);
+        } else {
+            clickListeners.delete(node);
+        }
+        render(node, content);
+    });
+
+    context.onRemove(() => {
+        const container = getOverlayDOMNode(props.key);
+        render(container, null);
+    });
+
+    return context.leave();
+}
+
+export default Object.assign(Overlay, {Portal});
diff --git a/src/ui/controls/overlay/style.less b/src/ui/controls/overlay/style.less
new file mode 100644
index 00000000..0c1c003f
--- /dev/null
+++ b/src/ui/controls/overlay/style.less
@@ -0,0 +1,18 @@
+@import "../../theme.less";
+
+.overlay {
+    align-items: center;
+    background-color: fade(@color-back, 50%);
+    flex-direction: column;
+    display: inline-flex;
+    height: 100%;
+    justify-content: center;
+    left: 0;
+    position: fixed;
+    top: 0;
+    width: 100%;
+
+    &:empty {
+        display: none;
+    }
+}
diff --git a/src/ui/controls/style.less b/src/ui/controls/style.less
index e56ba6f1..28f7f5ab 100644
--- a/src/ui/controls/style.less
+++ b/src/ui/controls/style.less
@@ -1,7 +1,9 @@
 @import "./button/style";
 @import "./checkbox/style";
 @import "./dropdown/style";
+@import "./message-box/style";
 @import "./multi-switch/style";
+@import "./overlay/style";
 @import "./shortcut/style";
 @import "./tab-panel/style";
 @import "./select/style";
diff --git a/src/ui/devtools/components/body.tsx b/src/ui/devtools/components/body.tsx
index ec92530e..0dbcce26 100644
--- a/src/ui/devtools/components/body.tsx
+++ b/src/ui/devtools/components/body.tsx
@@ -3,6 +3,7 @@ import {withState, useState} from 'malevic/state';
 import {Button} from '../../controls';
 import ThemeEngines from '../../../generators/theme-engines';
 import {DEVTOOLS_DOCS_URL} from '../../../utils/links';
+import {isFirefox} from '../../../utils/platform';
 import {ExtWrapper} from '../../../definitions';
 
 type BodyProps = ExtWrapper;
@@ -30,11 +31,30 @@ function Body({data, actions}: BodyProps) {
             reset: () => actions.resetDevDynamicThemeFixes(),
         });
 
-    function onTextRender(node) {
+    function onTextRender(node: HTMLTextAreaElement) {
         textNode = node;
         if (!state.errorText) {
             textNode.value = wrapper.fixesText;
         }
+        node.addEventListener('keydown', (e) => {
+            if (e.key === 'Tab') {
+                e.preventDefault();
+                const indent = ' '.repeat(4);
+                if (isFirefox()) {
+                    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220696
+                    const start = node.selectionStart;
+                    const end = node.selectionEnd;
+                    const before = node.value.substring(0, start);
+                    const after = node.value.substring(end);
+                    node.focus();
+                    node.value = `${before}${indent}${after}`;
+                    const cursorPos = start + indent.length;
+                    node.setSelectionRange(cursorPos, cursorPos);
+                } else {
+                    document.execCommand('insertText', false, indent);
+                }
+            }
+        });
     }
 
     async function apply() {
diff --git a/src/ui/popup/body/index.tsx b/src/ui/popup/body/index.tsx
index 621774fb..d8c77337 100644
--- a/src/ui/popup/body/index.tsx
+++ b/src/ui/popup/body/index.tsx
@@ -3,6 +3,7 @@ import {getContext} from 'malevic/dom';
 import {DONATE_URL} from '../../../utils/links';
 import {getLocalMessage} from '../../../utils/locales';
 import {isMobile} from '../../../utils/platform';
+import {Overlay} from '../../controls';
 import AutomationPage from '../automation-page';
 import MainPage from '../main-page';
 import {Page, PageViewer} from '../page-viewer';
@@ -122,6 +123,7 @@ export default function MobileBody(props: ViewProps) {
             <section class="m-section">
                 <DonateGroup />
             </section>
+            <Overlay />
         </body>
     );
 }
diff --git a/src/ui/popup/settings-page/reset-settings-button.tsx b/src/ui/popup/settings-page/reset-settings-button.tsx
index f9424946..728e32cb 100644
--- a/src/ui/popup/settings-page/reset-settings-button.tsx
+++ b/src/ui/popup/settings-page/reset-settings-button.tsx
@@ -1,19 +1,42 @@
 import {m} from 'malevic';
+import {getContext} from 'malevic/dom';
 import {DEFAULT_SETTINGS} from '../../../defaults';
-import {ResetButton} from '../../controls';
+import {MessageBox, ResetButton} from '../../controls';
 import ControlGroup from '../control-group';
 import {ViewProps} from '../types';
 
 export default function ResetButtonGroup(props: ViewProps) {
+    const context = getContext();
+
+    function showDialog() {
+        context.store.isDialogVisible = true;
+        context.refresh();
+    }
+
+    function hideDialog() {
+        context.store.isDialogVisible = false;
+        context.refresh();
+    }
+
     function reset() {
+        context.store.isDialogVisible = false;
         props.actions.changeSettings(DEFAULT_SETTINGS);
     }
 
+    const dialog = context.store.isDialogVisible ? (
+        <MessageBox
+            caption="Are you sure you want to remove all your settings? You cannot restore them later"
+            onOK={reset}
+            onCancel={hideDialog}
+        />
+    ) : null;
+
     return (
         <ControlGroup>
             <ControlGroup.Control>
-                <ResetButton onClick={reset}>
+                <ResetButton onClick={showDialog}>
                     Reset settings
+                    {dialog}
                 </ResetButton>
             </ControlGroup.Control>
             <ControlGroup.Description>
diff --git a/src/ui/popup/style.less b/src/ui/popup/style.less
index a967c4ac..0978e5b9 100644
--- a/src/ui/popup/style.less
+++ b/src/ui/popup/style.less
@@ -175,17 +175,36 @@ body {
     border: 2px solid white;
 }
 
+.overlay {
+    height: calc(100% - 4px);
+    left: 2px;
+    top: 2px;
+    width: calc(100% - 4px);
+}
+
 .built-in-horizontal-borders {
     body {
         border-bottom: none;
         border-top: none;
     }
+
+    .overlay {
+        height: 100%;
+        top: 0;
+    }
 }
 
 .built-in-borders {
     body {
         border: none;
     }
+
+    .overlay {
+        height: 100%;
+        left: 0;
+        top: 0;
+        width: 100%;
+    }
 }
 
 @import "./body/style";
diff --git a/src/ui/popup/theme/controls/mode.tsx b/src/ui/popup/theme/controls/mode.tsx
index 55fe7b0f..f237d6ba 100644
--- a/src/ui/popup/theme/controls/mode.tsx
+++ b/src/ui/popup/theme/controls/mode.tsx
@@ -8,6 +8,7 @@ export default function Mode(props: {mode: string; onChange: (mode: string) => v
     const modes = [
         [ThemeEngines.dynamicTheme, getLocalMessage('engine_dynamic')],
         [ThemeEngines.cssFilter, getLocalMessage('engine_filter')],
+        [ThemeEngines.svgFilter, getLocalMessage('engine_filter_plus')],
         [ThemeEngines.staticTheme, getLocalMessage('engine_static')],
     ];
     return (
diff --git a/src/ui/popup/theme/page/index.tsx b/src/ui/popup/theme/page/index.tsx
index ffd6cbac..ff787e4e 100644
--- a/src/ui/popup/theme/page/index.tsx
+++ b/src/ui/popup/theme/page/index.tsx
@@ -41,7 +41,7 @@ export default function ThemePage(props: ViewProps) {
                 onChange={(v) => props.actions.setTheme({scrollbarColor: v})}
                 onReset={() => props.actions.setTheme({scrollbarColor: DEFAULT_SETTINGS.theme.scrollbarColor})}
             />
-            <SelectionColorEditor 
+            <SelectionColorEditor
                 value={theme.selectionColor}
                 onChange={(v) => props.actions.setTheme({selectionColor: v})}
                 onReset={() => props.actions.setTheme({selectionColor: DEFAULT_SETTINGS.theme.selectionColor})}
diff --git a/src/utils/array.ts b/src/utils/array.ts
new file mode 100644
index 00000000..3f2a53fc
--- /dev/null
+++ b/src/utils/array.ts
@@ -0,0 +1,34 @@
+function isArrayLike<T>(items: Iterable<T> | ArrayLike<T>): items is ArrayLike<T> {
+    return (items as ArrayLike<T>).length != null;
+}
+
+// NOTE: Iterating Array-like items using `for .. of` is 3x slower in Firefox
+// https://jsben.ch/kidOp
+export function forEach<T>(items: Iterable<T> | ArrayLike<T>, iterator: (item: T) => void) {
+    if (isArrayLike(items)) {
+        for (let i = 0; i < items.length; i++) {
+            iterator(items[i]);
+        }
+    } else {
+        for (const item of items) {
+            iterator(item);
+        }
+    }
+}
+
+// NOTE: Pushing items like `arr.push(...items)` is 3x slower in Firefox
+// https://jsben.ch/nr9OF
+export function push<T>(array: Array<T>, addition: Iterable<T> | ArrayLike<T>) {
+    forEach(addition, (a) => array.push(a));
+}
+
+// NOTE: Using `Array.from()` is 2x (FF) — 5x (Chrome) slower for ArrayLike (not for Iterable)
+// https://jsben.ch/FJ1mO
+// https://jsben.ch/ZmViL
+export function toArray<T>(items: ArrayLike<T>) {
+    const results = [] as Array<T>;
+    for (let i = 0; i < items.length; i++) {
+        results.push(items[i]);
+    }
+    return results;
+}
diff --git a/src/utils/ipv6.ts b/src/utils/ipv6.ts
new file mode 100644
index 00000000..e62509ba
--- /dev/null
+++ b/src/utils/ipv6.ts
@@ -0,0 +1,17 @@
+/**
+ * Compares if IPV6 addresses are the same.
+ * @param firstIP First IP to compare with
+ * @param secondIP Second IP to compare with
+ */
+export function compareIPV6(firstIP: string, secondIP: string) {
+    firstIP = (firstIP
+        .replace(/^\^/, '') // Remove ^ at start
+        .replace(/\$$/, '') // Remove $ at end
+        .replace(/^.*?\/{2,3}/, '') // Remove scheme
+        .replace(/\?.*$/, '') // Remove query
+        .replace(/\/$/, '') // Remove last slash
+    );
+    const ip = secondIP.toLowerCase();
+    const ip2 = firstIP.toLowerCase();
+    return ip === ip2;
+}
diff --git a/src/utils/url.ts b/src/utils/url.ts
index 51097df2..300735ee 100644
--- a/src/utils/url.ts
+++ b/src/utils/url.ts
@@ -1,4 +1,5 @@
 import {UserSettings} from '../definitions';
+import {compareIPV6} from './ipv6';
 
 export function getURLHost(url: string) {
     return url.match(/^(.*?\/{2,3})?(.+?)(\/|$)/)[2];
@@ -28,8 +29,16 @@ export function isURLInList(url: string, list: string[]) {
  * @param urlTemplate URL template ("google.*", "youtube.com" etc).
  */
 export function isURLMatched(url: string, urlTemplate: string): boolean {
-    const regex = createUrlRegex(urlTemplate);
-    return Boolean(url.match(regex));
+    const isFirstIPV6 = url.includes('[');
+    const isSecondIPV6 = urlTemplate.includes('[');
+    if (isFirstIPV6 && isSecondIPV6) {
+        return compareIPV6(url, urlTemplate);
+    } else if (!isFirstIPV6 && ! isSecondIPV6){
+        const regex = createUrlRegex(urlTemplate);
+        return Boolean(url.match(regex));
+    } else {
+        return false;
+    }
 }
 
 function createUrlRegex(urlTemplate: string): RegExp {


Test patch:
diff --git a/tests/benchmark-server/index.js b/tests/benchmark-server/index.js
new file mode 100644
index 00000000..b3130406
--- /dev/null
+++ b/tests/benchmark-server/index.js
@@ -0,0 +1,150 @@
+// @ts-check
+const http = require('http');
+const url = require('url');
+
+const port = 1357;
+http.createServer((request, response) => {
+    const parsedurl = url.parse(request.url);
+    const query = parsedurl.query;
+    if (query == null) {
+        response.writeHead(404, {'Content-Type': 'text/plain'});
+        response.write('404 Not Found\n');
+        response.end();
+        return;
+    }
+    const type = query.includes('links') || query.includes('rules') || query.includes('styles') ? 'index' : query.startsWith('generated') ? 'css' : '404';
+    if (type === '404') {
+        response.writeHead(404, {'Content-Type': 'text/plain'});
+        response.write('404 Not Found\n');
+        response.end();
+        return;
+    }
+    if (type === 'index') {
+        response.writeHead(200, {'Content-Type': 'text/html'});
+        response.write(getHTML(query).join('\n'));
+        response.end();
+    } else {
+        response.writeHead(200, {'Content-Type': 'text/css'});
+        const parts = chunkString(CSS(query), 100);
+        const write = () => {
+            const chunk = parts.unshift();
+            if (chunk) {
+                response.write(chunk);
+                setTimeout(write, 10);
+            } else {
+                response.end();
+            }
+        };
+        write();
+    }
+
+}).listen(port, function () {
+    console.log(`The benchmark server has been opened on port ${port}`);
+});
+
+/**
+ * @param {string} str
+ * @param {string | number} length
+ */
+function chunkString(str, length) {
+    return str.match(new RegExp('.{1,' + length + '}', 'g'));
+}
+
+/**
+ * Returns an generated CSS style
+ *
+ * @param {string} query
+ */
+function CSS(query) {
+    const realquery = query.split('=');
+    const generated = parseInt(realquery[1]);
+    let result = '';
+    for (let x = 1; x <= generated; x++) {
+        result = result + `.GeneratedLinkElement${x} { background-color: black }\n`;
+    }
+    return result;
+}
+
+/**
+ * Generate a link to 'external css'
+ *
+ * @param {number} amount
+ */
+function link(amount) {
+    let result = '';
+    let element = '';
+    if (amount === 0) {
+        return result;
+    }
+    result = `\t\t\t<link rel="stylesheet" type="text/css" href="style.css?generated=${amount}">\n`;
+    for (let x = 1; x <= amount; x++) {
+        element = element + `\t\t<p class="GeneratedLinkElement${x}">This is an GeneratedLinkElement ${x}</p>\n`;
+    }
+    return [result, element];
+}
+
+/**
+ * Generate inline style elements
+ *
+ * @param {number} amount
+ */
+function style(amount) {
+    let result = '';;
+    if (amount === 0) {
+        return result;
+    }
+    for (let x = 1; x <= amount; x++) {
+        result = result + `\t\t<p style="background-color: green">This is an inline style element ${x}</p>\n`;
+    }
+    return result;
+}
+
+/**
+ * Generate a style
+ *
+ * @param {number} amount
+ */
+function rule(amount) {
+    let result = '';
+    let element = '';
+    if (amount === 0) {
+        return result;
+    }
+    result = result + '\t\t\t<style>';
+    for (let x = 1; x <= amount; x++) {
+        result = result + `.GeneratedElement${x} { background-color: red }\n`;
+        element = element + `\t\t<p class="GeneratedElement${x}">This is an GeneratedElement ${x}</p>\n`;
+    }
+    result = result + '</style>\n';
+    return [result, element];
+}
+
+/**
+ * @param {string} query
+ */
+function getHTML(query) {
+    const realquery = query.split('=').join('&').split('&');
+    const links = realquery[realquery.indexOf('links') + 1];
+    const styles = realquery[realquery.indexOf('styles') + 1];
+    const rules = realquery[realquery.indexOf('rules') + 1];
+    const linkHTML = link(parseInt(links));
+    const styleHTML = style(parseInt(styles));
+    const ruleHTML = rule(parseInt(rules));
+    const HTML = [];
+    HTML.push(
+        '<html>',
+        '   <head>',
+        '       <title>Benchmark Server</title>',
+        ruleHTML[0],
+        linkHTML[0],
+        '   </head>',
+        '   <body>',
+        styleHTML,
+        linkHTML[1],
+        ruleHTML[1],
+        '   </body>',
+        '</html>',
+    );
+    return HTML;
+
+}
diff --git a/tests/url.tests.ts b/tests/url.tests.ts
index a0b78348..6b59afbe 100644
--- a/tests/url.tests.ts
+++ b/tests/url.tests.ts
@@ -125,6 +125,48 @@ test('URL is enabled', () => {
         'https://www.google.com/very/good/hidden/folder/pdf#file.pdf'
     )).toBe(false);
 
+    // IPV6 Testing
+    expect(isURLEnabled(
+        '[::1]:1337',
+        {siteList: ['google.com'], siteListEnabled: [], applyToListedOnly: false} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toBe(true);
+    expect(isURLEnabled(
+        '[::1]:8080',
+        {siteList: ['[::1]:8080'], siteListEnabled: [], applyToListedOnly: false} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toEqual(false);
+    expect(isURLEnabled(
+        '[::1]:8080',
+        {siteList: ['[::1]:8081'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toEqual(false);
+    expect(isURLEnabled(
+        '[::1]:8080',
+        {siteList: ['[::1]:8081'], siteListEnabled: [], applyToListedOnly: false} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toEqual(true);
+    expect(isURLEnabled(
+        '[::1]:17',
+        {siteList: ['[::1]'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toEqual(false);
+    expect(isURLEnabled(
+        '[2001:4860:4860::8888]',
+        {siteList: ['[2001:4860:4860::8888]'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toEqual(true);
+    expect(isURLEnabled(
+        '[2001:4860:4860::8844]',
+        {siteList: ['[2001:4860:4860::8844]'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
+        {isProtected: false, isInDarkList: true},
+    )).toEqual(true);
+    expect(isURLEnabled(
+        '[2001:4860:4860::8844]',
+        {siteList: [], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
+        {isProtected: false, isInDarkList: true},
+    )).toEqual(false);
+
     // Temporary Dark Sites list fix
     expect(isURLEnabled(
         'https://darkreader.org/',