Repository: darkreader/darkreader
Pull request #3157 (2020-07-19 18:12:17)

FIxed my.nintendo.com
- Optimized.


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 2502f633..1cb29cfd 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -5,6 +5,10 @@ module.exports = {
     rules: {
         'array-bracket-spacing': ['error', 'never'],
         'block-spacing': ['error', 'always'],
+        'comma-spacing': ['error', {
+            before: false,
+            after: true,
+        }],
         'indent': ['error', 4, {
             'SwitchCase': 1,
         }],
diff --git a/package-lock.json b/package-lock.json
index 228faaf6..442cb1fe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -901,9 +901,9 @@
       }
     },
     "@types/chrome": {
-      "version": "0.0.120",
-      "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.120.tgz",
-      "integrity": "sha512-dFA5nxeccCWqPUr5y5mSKiwxIwHkySx6IAnJ2wBbnmsVvtKHuiAHpS0Cq8hyv/4aUR0KdhPD0Pp0o99E39rzFA==",
+      "version": "0.0.121",
+      "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.121.tgz",
+      "integrity": "sha512-kt1gZrUnzCARcLIl9rb2+mW15YsijPgNlyAbumB1uZ5XakohAjh8+bD0kfGl462E154Hdn1p2WdKEGizOJ4VgA==",
       "dev": true,
       "requires": {
         "@types/filesystem": "*",
@@ -984,9 +984,9 @@
       }
     },
     "@types/jest": {
-      "version": "26.0.4",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.4.tgz",
-      "integrity": "sha512-4fQNItvelbNA9+sFgU+fhJo8ZFF+AS4Egk3GWwCW2jFtViukXbnztccafAdLhzE/0EiCogljtQQXP8aQ9J7sFg==",
+      "version": "26.0.5",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.5.tgz",
+      "integrity": "sha512-heU+7w8snfwfjtcj2H458aTx3m5unIToOJhx75ebHilBiiQ39OIdA18WkG4LP08YKeAoWAGvWg8s+22w/PeJ6w==",
       "dev": true,
       "requires": {
         "jest-diff": "^25.2.1",
@@ -1476,9 +1476,9 @@
       }
     },
     "binary-extensions": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
-      "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+      "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
       "dev": true
     },
     "brace-expansion": {
@@ -1603,9 +1603,9 @@
       "dev": true
     },
     "chokidar": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
-      "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz",
+      "integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==",
       "dev": true,
       "requires": {
         "anymatch": "~3.1.1",
@@ -2056,9 +2056,9 @@
       }
     },
     "eslint": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.4.0.tgz",
-      "integrity": "sha512-gU+lxhlPHu45H3JkEGgYhWhkR9wLHHEXC9FbWFnTlEkbKyZKWgWRLgf61E8zWmBuI6g5xKBph9ltg3NtZMVF8g==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.5.0.tgz",
+      "integrity": "sha512-vlUP10xse9sWt9SGRtcr1LAC67BENcQMFeV+w5EvLEoFe3xJ8cF1Skd0msziRx/VMC+72B4DxreCE+OR12OA6Q==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
@@ -2069,9 +2069,9 @@
         "doctrine": "^3.0.0",
         "enquirer": "^2.3.5",
         "eslint-scope": "^5.1.0",
-        "eslint-utils": "^2.0.0",
-        "eslint-visitor-keys": "^1.2.0",
-        "espree": "^7.1.0",
+        "eslint-utils": "^2.1.0",
+        "eslint-visitor-keys": "^1.3.0",
+        "espree": "^7.2.0",
         "esquery": "^1.2.0",
         "esutils": "^2.0.2",
         "file-entry-cache": "^5.0.1",
@@ -2085,7 +2085,7 @@
         "js-yaml": "^3.13.1",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.4.1",
-        "lodash": "^4.17.14",
+        "lodash": "^4.17.19",
         "minimatch": "^3.0.4",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.1",
@@ -2131,14 +2131,14 @@
       "dev": true
     },
     "espree": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-7.1.0.tgz",
-      "integrity": "sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz",
+      "integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==",
       "dev": true,
       "requires": {
-        "acorn": "^7.2.0",
+        "acorn": "^7.3.1",
         "acorn-jsx": "^5.2.0",
-        "eslint-visitor-keys": "^1.2.0"
+        "eslint-visitor-keys": "^1.3.0"
       }
     },
     "esprima": {
@@ -4246,9 +4246,9 @@
       "dev": true
     },
     "less": {
-      "version": "3.12.0",
-      "resolved": "https://registry.npmjs.org/less/-/less-3.12.0.tgz",
-      "integrity": "sha512-3mmSHFRP9hGxxQgAKgChfau1LO3ksV/zyZf1qd2ENyBV778NA9Ids99wFRA20jE+5prT7oScKod8PoGlxSe1gg==",
+      "version": "3.12.2",
+      "resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
+      "integrity": "sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q==",
       "dev": true,
       "requires": {
         "errno": "^0.1.1",
@@ -4372,9 +4372,9 @@
       }
     },
     "malevic": {
-      "version": "0.18.5",
-      "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.18.5.tgz",
-      "integrity": "sha512-7fzRV8qSlYQ04hw7c77A/JTCHdqO6dbI9PcgL780C9Toad4sRr/aAEGfhjA2VmW+iOmFiKxcYBtDjx6ShbodOQ==",
+      "version": "0.18.6",
+      "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.18.6.tgz",
+      "integrity": "sha512-Um4XRYJpVDhKjRRteiuHdDmcNbI5gX7URsXC6G+5Tk0Dai2W2RB39kg5C/M32IezNPudT+YsgApBh8JG6fIWrA==",
       "dev": true
     },
     "map-cache": {
@@ -4479,10 +4479,13 @@
       }
     },
     "mkdirp": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
-      "dev": true
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
     },
     "ms": {
       "version": "2.1.2",
@@ -4510,9 +4513,9 @@
       }
     },
     "native-request": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.5.tgz",
-      "integrity": "sha512-7wU3DvBGAJQxWuMR3F62zrhB7hxNj2DdlC/eBVrCgavc6+ZpFZOqS/PsR7QyUPLMkFk0GvvzoeeOAZGLLnObnA==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.7.tgz",
+      "integrity": "sha512-9nRjinI9bmz+S7dgNtf4A70+/vPhnd+2krGpy4SUlADuOuSa24IDkNaZ+R/QT1wQ6S8jBdi6wE7fLekFZNfUpQ==",
       "dev": true,
       "optional": true
     },
@@ -5141,9 +5144,9 @@
       }
     },
     "rollup": {
-      "version": "2.21.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.21.0.tgz",
-      "integrity": "sha512-BEGgy+wSzux7Ycq58pRiWEOBZaXRXTuvzl1gsm7gqmsAHxkWf9nyA5V2LN9fGSHhhDQd0/C13iRzSh4bbIpWZQ==",
+      "version": "2.22.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.22.1.tgz",
+      "integrity": "sha512-K9AUQUCJkVqC+A7uUDacfhmpEeAjc2uOmSpvGI5xaYsm8pXgy4ZWEM8wHPfKj11xvCwFZppkRDo8a0RESJXCnw==",
       "dev": true,
       "requires": {
         "fsevents": "~2.1.2"
@@ -5838,9 +5841,9 @@
       "dev": true
     },
     "strip-json-comments": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz",
-      "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
       "dev": true
     },
     "supports-color": {
@@ -6027,9 +6030,9 @@
       }
     },
     "ts-jest": {
-      "version": "26.1.2",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.2.tgz",
-      "integrity": "sha512-V4SyBDO9gOdEh+AF4KtXJeP+EeI4PkOrxcA8ptl4o8nCXUVM5Gg/8ngGKneS5BsZaR9DXVQNqj9k+iqGAnpGow==",
+      "version": "26.1.3",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.3.tgz",
+      "integrity": "sha512-beUTSvuqR9SmKQEylewqJdnXWMVGJRFqSz2M8wKJe7GBMmLZ5zw6XXKSJckbHNMxn+zdB3guN2eOucSw2gBMnw==",
       "dev": true,
       "requires": {
         "bs-logger": "0.x",
@@ -6042,6 +6045,14 @@
         "mkdirp": "1.x",
         "semver": "7.x",
         "yargs-parser": "18.x"
+      },
+      "dependencies": {
+        "mkdirp": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+          "dev": true
+        }
       }
     },
     "ts-node": {
@@ -6147,9 +6158,9 @@
       }
     },
     "typescript": {
-      "version": "3.9.6",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz",
-      "integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==",
+      "version": "3.9.7",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
+      "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
       "dev": true
     },
     "union-value": {
@@ -6395,17 +6406,6 @@
       "dev": true,
       "requires": {
         "mkdirp": "^0.5.1"
-      },
-      "dependencies": {
-        "mkdirp": {
-          "version": "0.5.5",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
-          "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
-          "dev": true,
-          "requires": {
-            "minimist": "^1.2.5"
-          }
-        }
       }
     },
     "write-file-atomic": {
diff --git a/package.json b/package.json
index f9403337..35ec4f46 100644
--- a/package.json
+++ b/package.json
@@ -38,26 +38,26 @@
     "@rollup/plugin-node-resolve": "8.4.0",
     "@rollup/plugin-replace": "2.3.3",
     "@rollup/pluginutils": "3.1.0",
-    "@types/chrome": "0.0.120",
-    "@types/jest": "26.0.4",
+    "@types/chrome": "0.0.121",
+    "@types/jest": "26.0.5",
     "@types/node": "14.0.23",
     "@typescript-eslint/eslint-plugin": "3.6.1",
     "@typescript-eslint/parser": "3.6.1",
-    "chokidar": "3.4.0",
-    "eslint": "7.4.0",
+    "chokidar": "3.4.1",
+    "eslint": "7.5.0",
     "eslint-plugin-local": "1.0.0",
     "fs-extra": "9.0.1",
     "globby": "11.0.1",
     "jest": "26.1.0",
-    "less": "3.12.0",
-    "malevic": "0.18.5",
+    "less": "3.12.2",
+    "malevic": "0.18.6",
     "prettier": "2.0.5",
-    "rollup": "2.21.0",
+    "rollup": "2.22.1",
     "rollup-plugin-typescript2": "0.27.1",
-    "ts-jest": "26.1.2",
+    "ts-jest": "26.1.3",
     "ts-node": "8.10.2",
     "tsconfig-paths": "3.9.0",
-    "typescript": "3.9.6",
+    "typescript": "3.9.7",
     "ws": "7.3.1",
     "yazl": "2.5.1"
   }
diff --git a/src/background/tab-manager.ts b/src/background/tab-manager.ts
index 996885d0..036f16e0 100644
--- a/src/background/tab-manager.ts
+++ b/src/background/tab-manager.ts
@@ -79,6 +79,13 @@ export default class TabManager {
                 a.download = name;
                 a.click();
             }
+            if (type === 'request-export-css') {
+                const activeTab = await this.getActiveTab();
+                this.ports
+                    .get(activeTab.id)
+                    .get(0).port
+                    .postMessage({type: 'export-css'});
+            }
         });
     }
 
@@ -111,6 +118,9 @@ export default class TabManager {
     }
 
     async getActiveTabURL() {
+        return (await this.getActiveTab()).url;
+    }
+    async getActiveTab() {
         let tab = (await queryTabs({
             active: true,
             lastFocusedWindow: true
@@ -121,6 +131,6 @@ export default class TabManager {
             const tabs = (await queryTabs({active: true}));
             tab = tabs.find((t) => !isExtensionPage(t.url)) || tab;
         }
-        return tab.url;
+        return tab;
     }
 }
diff --git a/src/config/dynamic-theme-fixes.config b/src/config/dynamic-theme-fixes.config
index 49c4f81a..5503b74b 100644
--- a/src/config/dynamic-theme-fixes.config
+++ b/src/config/dynamic-theme-fixes.config
@@ -136,6 +136,14 @@ CSS
 
 ================================
 
+abiturma.de
+
+INVERT
+div.formula
+amp-img.latex-inline
+
+================================
+
 ableton.com
 
 INVERT
@@ -695,6 +703,16 @@ CSS
 
 ================================
 
+chilkatsoft.com
+
+CSS
+body {
+    background-image: none !important;
+    background-color: var(--darkreader-neutral-background) !important;   
+}
+
+================================
+
 chtoes.li
 
 INVERT
@@ -846,16 +864,8 @@ coolblue.be
 coolblue.nl
 hotorangemedia.nl
 
-CSS
-path[fill="#F60"] {
-    --darkreader-inline-fill: #F60 !important
-}
-path[fill="#FFF"] {
-    --darkreader-inline-fill: #FFF !important
-}
-path[fill="#285DAB"] {
-    --darkreader-inline-fill: #285DAB !important
-}
+IGNORE INLINE STYLE
+.header__logo-image > path
 
 ================================
 
@@ -875,6 +885,16 @@ img[title="Switch"]
 
 ================================
 
+copitosystem.com
+
+CSS
+body {
+    background-image: none !important;
+    background-color: var(--darkreader-neutral-background) !important;
+}
+
+================================
+
 courses.fit.cvut.cz
 
 CSS
@@ -1546,6 +1566,18 @@ img[src*="map"], [style*="map"],
 .sp_M4ukGb_suKT {
     background-image: url(/rsrc.php/v3/yA/r/wFJuszm5vxY.png) !important;
 }
+.sp_PSWLq4kgoAU {
+    background-image: url(https://static.xx.fbcdn.net/rsrc.php/v3/y-/r/f0k1WJGEgri.png) !important;;
+}
+.sp_TbPEy0vJCwv {
+    background-image: url(/rsrc.php/v3/yi/r/6CkpTuf5j8w.png) !important;
+}
+.sp_0Oe4oGgXSQb {
+    background-image: url(/rsrc.php/v3/yD/r/Nt7s6Wvp3Df.png) !important;
+}
+.sp_5vRJx47G9C5 {
+    background-image: url(/rsrc.php/v3/yi/r/rU9AJ4lp7mV.png) !important;
+}
 
 IGNORE INLINE STYLE
 [role="button"] svg
@@ -1891,21 +1923,21 @@ CSS
     fill: ${#ebedf0} !important;
     background-color: ${#ebedf0} !important;
 }
-.day[fill="#c6e48b"], .legend li[style*="background-color: rgb(198, 228, 139)"] {
-    fill: ${#c6d4cb} !important;
-    background-color: ${#c6d4cb} !important;
+.day[fill="#9be9a8"], .legend li[style*="background-color: rgb(155, 233, 168)"] {
+    fill: ${#9be9a8} !important;
+    background-color: ${#9be9a8} !important;
 }
-.day[fill="#7bc96f"], .legend li[style*="background-color: rgb(123, 201, 111)"] {
-    fill: ${#7bb97f} !important;
-    background-color: ${#7bb97f} !important;
+.day[fill="#40c463"], .legend li[style*="background-color: rgb(64, 196, 99)"] {
+    fill: ${#40c463} !important;
+    background-color: ${#40c463} !important;
 }
-.day[fill="#239a3b"], .legend li[style*="background-color: rgb(35, 154, 59)"] {
-    fill: ${#238a3b} !important;
-    background-color: ${#238a3b} !important;
+.day[fill="#30a14e"], .legend li[style*="background-color: rgb(48, 161, 78)"] {
+    fill: ${#30a14e} !important;
+    background-color: ${#30a14e} !important;
 }
-.day[fill="#196127"], .legend li[style*="background-color: rgb(25, 97, 39)"] {
-    fill: ${#196127} !important;
-    background-color: ${#196127} !important;
+.day[fill="#216e39"], .legend li[style*="background-color: rgb(33, 110, 57)"] {
+    fill: ${#216e39} !important;
+    background-color: ${#216e39} !important;
 }
 .blob-num:not(.cc-coverage-covered-border):not(.cc-coverage-missed-border) {
     border-right: 0 !important;
@@ -2445,6 +2477,13 @@ INVERT
 
 ================================
 
+jsdelivr.com
+
+INVERT
+.navbar-brand > img
+
+================================
+
 kaggle.com
 kaggleusercontent.com
 
@@ -2475,6 +2514,15 @@ INVERT
 
 ================================
 
+krita.org
+
+CSS
+.container {
+    background: none !important;
+}
+
+================================
+
 laptopmag.com
 
 INVERT
@@ -2733,6 +2781,13 @@ INVERT
 
 ================================
 
+maps.metager.de
+
+INVERT
+.map canvas
+
+================================
+
 marginalrevolution.com
 
 INVERT
@@ -3072,10 +3127,8 @@ a.d-none.d-md-inline
 
 my.nintendo.com
 
-CSS
-.Layout-app {
-    background-image: url(https://svgshare.com/i/HcN.svg);
-}
+IGNORE IMAGE ANALYSIS
+.Layout-app
 
 ================================
 
@@ -3102,10 +3155,8 @@ INVERT
 div.logo.logo-slim
 div.logo.logo-large.logo-footer
 
-CSS
-.logo {
-    background-image: url(/pb/resources/images/wwbt-logo.svg?v=92) !important;
-}
+IGNORE IMAGE ANALYSIS
+.logo
 
 ================================
 
@@ -3238,6 +3289,18 @@ CSS
 
 ================================
 
+nvidia.com
+
+INVERT
+.global-footer__logo
+
+CSS
+.brand-link.pull-left {
+    filter: invert(1) hue-rotate(170deg) brightness(4);
+}
+
+================================
+
 nvidia.com
 nvidia.in
 
@@ -3438,7 +3501,7 @@ INVERT
 openai.com
 
 INVERT
-.release-cover-title img
+figure.release-cover>div:first-of-type>img
 
 CSS
 body {
@@ -3535,6 +3598,16 @@ CSS
 
 ================================
 
+peonaviveu.blogspot.com
+
+CSS
+body {
+    background-image: none !important;    
+    background-color: var(--darkreader-neutral-background) !important;                                                                                                                                                                                                                                                                     
+}
+
+================================
+
 pgatour.com
 
 CSS
@@ -3733,6 +3806,18 @@ CSS
 [style^="--pseudo-before-background"] {
     --pseudo-before-background: ${#DAE0E6} !important;
 }
+.md p>a[href="#s"]::after, a[href="#s"]::after {
+    color: #000;
+}
+
+================================
+
+redgamingtech.com
+
+CSS
+body {
+    background-image: none !important;                                                                                                                                                                                                                                                                    
+}
 
 ================================
 
@@ -4029,6 +4114,15 @@ CSS
 
 ================================
 
+snack.expo.io
+
+CSS
+#root > div > div > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div > div:nth-child(2) {
+    background-color: white !important;
+}
+
+================================
+
 softorage.com
 
 CSS
@@ -4038,6 +4132,13 @@ CSS
 
 ================================
 
+sony.*
+
+IGNORE INLINE STYLE
+.brand-logo-svg > g
+
+================================
+
 soundcloud.com
 
 INVERT
@@ -4097,6 +4198,15 @@ CSS
 
 ================================
 
+stardewvalleywiki.com
+
+CSS
+html {
+    background: none !important;
+}
+
+================================
+
 store.ubi.com
 
 INVERT
@@ -4558,19 +4668,11 @@ INVERT
 .ita-hwt-grip
 .gt-ex-quote
 
-CSS
-.copybutton .jfk-button-img {
-    background-image: -webkit-image-set(url(https://ssl.gstatic.com/images/icons/material/system_gm/1x/content_copy_black_24dp.png) 1x,url(https://ssl.gstatic.com/images/icons/material/system_gm/2x/content_copy_black_24dp.png) 2x) !important;
-}
-.speech-button .jfk-button-img {
-    background-image: -webkit-image-set(url(https://ssl.gstatic.com/images/icons/material/system_gm/1x/mic_black_24dp.png) 1x,url(https://ssl.gstatic.com/images/icons/material/system_gm/2x/mic_black_24dp.png) 2x) !important;
-}
-.result .unstarred > .jfk-button-img {
-    background-image: -webkit-image-set(url(https://ssl.gstatic.com/images/icons/material/system_gm/1x/star_border_black_24dp.png) 1x,url(https://ssl.gstatic.com/images/icons/material/system_gm/2x/star_border_black_24dp.png) 2x) !important;
-}
-.morebutton .jfk-button-img {
-    background-image: -webkit-image-set(url(https://ssl.gstatic.com/images/icons/material/system_gm/1x/more_vert_black_24dp.png) 1x,url(https://ssl.gstatic.com/images/icons/material/system_gm/2x/more_vert_black_24dp.png) 2x) !important;
-}
+IGNORE IMAGE ANALYSIS
+.copybutton .jfk-button-img
+.speech-button .jfk-button-img
+.result .unstarred > .jfk-button-img
+.morebutton .jfk-button-img
 
 ================================
 
@@ -4836,6 +4938,19 @@ CSS
 
 unsplash.com
 
+INVERT
+._1Jlgk
+._1Ig-9
+
+CSS
+._2AYJu._1PdBr {
+    background: ${white} !important;
+}
+
+================================
+
+unsplash.com
+
 CSS
 ._2AYJu._1PdBr {
     background: ${white} !important;
diff --git a/src/definitions.d.ts b/src/definitions.d.ts
index 0269576a..5911c88d 100644
--- a/src/definitions.d.ts
+++ b/src/definitions.d.ts
@@ -116,9 +116,10 @@ export interface Shortcuts {
 
 export interface DynamicThemeFix {
     url: string[];
-    ignoreInlineStyle: string[];
     invert: string[];
     css: string;
+    ignoreInlineStyle: string[];
+    ignoreImageAnalysis: string[];
 }
 
 export interface InversionFix {
diff --git a/src/generators/dynamic-theme.ts b/src/generators/dynamic-theme.ts
index ce68d56d..49a4e0a5 100644
--- a/src/generators/dynamic-theme.ts
+++ b/src/generators/dynamic-theme.ts
@@ -8,6 +8,7 @@ const dynamicThemeFixesCommands = {
     'INVERT': 'invert',
     'CSS': 'css',
     'IGNORE INLINE STYLE': 'ignoreInlineStyle',
+    'IGNORE IMAGE ANALYSIS': 'ignoreImageAnalysis',
 };
 
 export function parseDynamicThemeFixes(text: string) {
@@ -54,6 +55,7 @@ export function getDynamicThemeFixesFor(url: string, frameURL: string, fixes: Dy
         invert: fixes[0].invert || [],
         css: fixes[0].css || [],
         ignoreInlineStyle: fixes[0].ignoreInlineStyle || [],
+        ignoreImageAnalysis: fixes[0].ignoreImageAnalysis || [],
     };
     if (enabledForPDF) {
         common.invert = common.invert.concat('embed[type="application/pdf"]');
@@ -80,5 +82,6 @@ export function getDynamicThemeFixesFor(url: string, frameURL: string, fixes: Dy
         invert: common.invert.concat(match.invert || []),
         css: [common.css, match.css].filter((s) => s).join('\n'),
         ignoreInlineStyle: common.ignoreInlineStyle.concat(match.ignoreInlineStyle || []),
+        ignoreImageAnalysis: common.ignoreImageAnalysis.concat(match.ignoreImageAnalysis || []),
     };
 }
diff --git a/src/inject/dynamic-theme/adopted-style-manger.ts b/src/inject/dynamic-theme/adopted-style-manger.ts
index dde7186b..4326b6d8 100644
--- a/src/inject/dynamic-theme/adopted-style-manger.ts
+++ b/src/inject/dynamic-theme/adopted-style-manger.ts
@@ -6,7 +6,7 @@ const adoptedStyleOverrides = new WeakMap<CSSStyleSheet, CSSStyleSheet>();
 const overrideList = new WeakSet<CSSStyleSheet>();
 
 export interface AdoptedStyleSheetManager {
-    render(theme: Theme, variables: Map<string, string>): void;
+    render(theme: Theme, variables: Map<string, string>, ignoreImageAnalysis: string[]): void;
     destroy(): void;
 }
 
@@ -44,7 +44,7 @@ export function createAdoptedStyleSheetOverride(node: Document | ShadowRoot): Ad
         node.adoptedStyleSheets = newSheets;
     }
 
-    function render(theme: Theme, globalVariables: Map<string, string>) {
+    function render(theme: Theme, globalVariables: Map<string, string>, ignoreImageAnalysis: string[]) {
         node.adoptedStyleSheets.forEach((sheet) => {
             if (overrideList.has(sheet)) {
                 return;
@@ -72,6 +72,7 @@ export function createAdoptedStyleSheetOverride(node: Document | ShadowRoot): Ad
                 sourceCSSRules: rules,
                 theme,
                 variables,
+                ignoreImageAnalysis,
                 force: false,
                 isAsyncCancelled: () => cancelAsyncOperations,
             });
diff --git a/src/inject/dynamic-theme/css-collection.ts b/src/inject/dynamic-theme/css-collection.ts
new file mode 100644
index 00000000..b373d448
--- /dev/null
+++ b/src/inject/dynamic-theme/css-collection.ts
@@ -0,0 +1,51 @@
+import {forEach} from '../../utils/array';
+import {loadAsDataURL} from '../../utils/network';
+import {getMatches, formatCSS} from '../../utils/text';
+
+const blobRegex = /url\(\"(blob\:.*?)\"\)/g;
+
+async function replaceBlobs(text: string) {
+    const promises = [];
+    getMatches(blobRegex, text, 1).forEach((url) => {
+        const promise = loadAsDataURL(url);
+        promises.push(promise);
+    });
+    const data = await Promise.all(promises);
+    return text.replace(blobRegex, () => `url("${data.shift()}")`);
+}
+
+export async function collectCSS() {
+    const css = [];
+
+    function addStaticCSS(selector: string, comment: string) {
+        const staticStyle = document.head.querySelector(selector);
+        if (staticStyle && staticStyle.textContent) {
+            css.push(`/* ${comment} */`);
+            css.push(staticStyle.textContent);
+            css.push('');
+        }
+    }
+
+    addStaticCSS('.darkreader--fallback', 'Fallback Style');
+    addStaticCSS('.darkreader--user-agent', 'User-Agent Style');
+    addStaticCSS('.darkreader--text', 'Text Style');
+    addStaticCSS('.darkreader--invert', 'Invert Style');
+    addStaticCSS('.darkreader--override', 'Override Style');
+    addStaticCSS('.darkreader--variables', 'Variables Style');
+
+    const modifiedCSS = [];
+    document.querySelectorAll('.darkreader--sync').forEach((element: HTMLStyleElement) => {
+        forEach(element.sheet.cssRules, (rule) => {
+            rule && rule.cssText && modifiedCSS.push(rule.cssText);
+        });
+    });
+
+    if (modifiedCSS.length != 0) {
+        const formattedCSS = formatCSS(modifiedCSS.join('\n'));
+        css.push('/* Modified CSS */');
+        css.push(await replaceBlobs(formattedCSS));
+        css.push('');
+    }
+
+    return css.join('\n');
+}
diff --git a/src/inject/dynamic-theme/index.ts b/src/inject/dynamic-theme/index.ts
index ca73e1de..8a03cd16 100644
--- a/src/inject/dynamic-theme/index.ts
+++ b/src/inject/dynamic-theme/index.ts
@@ -137,6 +137,10 @@ function cleanFallbackStyle() {
     }
 }
 
+function getIgnoreImageAnalysisSelectors() {
+    return fixes && Array.isArray(fixes.ignoreImageAnalysis) ? fixes.ignoreImageAnalysis : [];
+}
+
 function createDynamicStyleOverrides() {
     cancelRendering();
 
@@ -152,7 +156,7 @@ function createDynamicStyleOverrides() {
         .filter((details) => details && details.variables.size > 0)
         .map(({variables}) => variables);
     if (newVariables.length === 0) {
-        styleManagers.forEach((manager) => manager.render(filter, variables));
+        styleManagers.forEach((manager) => manager.render(filter, variables, getIgnoreImageAnalysisSelectors()));
         if (loadingStyles.size === 0) {
             cleanFallbackStyle();
         }
@@ -174,8 +178,8 @@ function createDynamicStyleOverrides() {
             push(inlineStyleElements, elements);
         }
     });
-    const ignoredSelectors = fixes && Array.isArray(fixes.ignoreInlineStyle) ? fixes.ignoreInlineStyle : [];
-    inlineStyleElements.forEach((el) => overrideInlineStyle(el as HTMLElement, filter, ignoredSelectors));
+    const ignoredInlineSelectors = fixes && Array.isArray(fixes.ignoreInlineStyle) ? fixes.ignoreInlineStyle : [];
+    inlineStyleElements.forEach((el) => overrideInlineStyle(el as HTMLElement, filter, getIgnoreImageAnalysisSelectors(), ignoredInlineSelectors));
     handleAdoptedStyleSheets(document);
 }
 
@@ -209,7 +213,7 @@ function createManager(element: StyleElement) {
             return;
         }
         if (details.variables.size === 0) {
-            manager.render(filter, variables);
+            manager.render(filter, variables, getIgnoreImageAnalysisSelectors());
         } else {
             updateVariables(details.variables);
             throttledRenderAllStyles();
@@ -243,8 +247,8 @@ function removeManager(element: StyleElement) {
 }
 
 const throttledRenderAllStyles = throttle((callback?: () => void) => {
-    styleManagers.forEach((manager) => manager.render(filter, variables));
-    adoptedStyleManagers.forEach((manager) => manager.render(filter, variables));
+    styleManagers.forEach((manager) => manager.render(filter, variables, getIgnoreImageAnalysisSelectors()));
+    adoptedStyleManagers.forEach((manager) => manager.render(filter, variables, getIgnoreImageAnalysisSelectors()));
     callback && callback();
 });
 
@@ -301,8 +305,9 @@ function handleAdoptedStyleSheets(node: ShadowRoot | Document) {
     if (Array.isArray(node.adoptedStyleSheets)) {
         if (node.adoptedStyleSheets.length > 0) {
             const newManger = createAdoptedStyleSheetOverride(node);
+
             adoptedStyleManagers.push(newManger);
-            newManger.render(filter, variables);
+            newManger.render(filter, variables, getIgnoreImageAnalysisSelectors());
         }
     }
 }
@@ -323,7 +328,7 @@ function watchForUpdates() {
             .filter((details) => details && details.variables.size > 0)
             .map(({variables}) => variables);
         if (newVariables.length === 0) {
-            newManagers.forEach((manager) => manager.render(filter, variables));
+            newManagers.forEach((manager) => manager.render(filter, variables, getIgnoreImageAnalysisSelectors()));
         } else {
             newVariables.forEach((variables) => updateVariables(variables));
             throttledRenderAllStyles();
@@ -334,9 +339,9 @@ function watchForUpdates() {
         handleAdoptedStyleSheets(shadowRoot);
     });
 
-    const ignoredSelectors = fixes && Array.isArray(fixes.ignoreInlineStyle) ? fixes.ignoreInlineStyle : [];
+    const ignoredInlineSelectors = fixes && Array.isArray(fixes.ignoreInlineStyle) ? fixes.ignoreInlineStyle : [];
     watchForInlineStyles((element) => {
-        overrideInlineStyle(element, filter, ignoredSelectors);
+        overrideInlineStyle(element, filter, ignoredInlineSelectors, getIgnoreImageAnalysisSelectors());
         if (element === document.documentElement) {
             const rootVariables = getElementCSSVariables(document.documentElement);
             if (rootVariables.size > 0) {
@@ -348,7 +353,7 @@ function watchForUpdates() {
         const inlineStyleElements = root.querySelectorAll(INLINE_STYLE_SELECTOR);
         if (inlineStyleElements.length > 0) {
             createShadowStaticStyleOverrides(root);
-            forEach(inlineStyleElements, (el) => overrideInlineStyle(el as HTMLElement, filter, ignoredSelectors));
+            forEach(inlineStyleElements, (el) => overrideInlineStyle(el as HTMLElement, filter, getIgnoreImageAnalysisSelectors(), ignoredInlineSelectors));
         }
     });
 
diff --git a/src/inject/dynamic-theme/inline-style.ts b/src/inject/dynamic-theme/inline-style.ts
index 32ca68b4..024371fb 100644
--- a/src/inject/dynamic-theme/inline-style.ts
+++ b/src/inject/dynamic-theme/inline-style.ts
@@ -213,7 +213,7 @@ function shouldIgnoreInlineStyle(element: HTMLElement, selectors: string[]) {
     return false;
 }
 
-export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, ignoreSelectors: string[]) {
+export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, ignoreInlineSelectors: string[], ignoreImageSelectors: string[]) {
     const cacheKey = getInlineStyleCacheKey(element, theme);
     if (cacheKey === inlineStyleCache.get(element)) {
         return;
@@ -224,7 +224,7 @@ export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, i
     function setCustomProp(targetCSSProp: string, modifierCSSProp: string, cssVal: string) {
         const {customProp, dataAttr} = overrides[targetCSSProp];
 
-        const mod = getModifiableCSSDeclaration(modifierCSSProp, cssVal, null, null);
+        const mod = getModifiableCSSDeclaration(modifierCSSProp, cssVal, null, ignoreImageSelectors, null);
         if (!mod) {
             return;
         }
@@ -239,8 +239,8 @@ export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, i
         unsetProps.delete(targetCSSProp);
     }
 
-    if (ignoreSelectors.length > 0) {
-        if (shouldIgnoreInlineStyle(element, ignoreSelectors)) {
+    if (ignoreInlineSelectors.length > 0) {
+        if (shouldIgnoreInlineStyle(element, ignoreInlineSelectors)) {
             unsetProps.forEach((cssProp) => {
                 const {store, dataAttr} = overrides[cssProp];
                 store.delete(element);
diff --git a/src/inject/dynamic-theme/modify-css.ts b/src/inject/dynamic-theme/modify-css.ts
index 589c294e..0a32a7df 100644
--- a/src/inject/dynamic-theme/modify-css.ts
+++ b/src/inject/dynamic-theme/modify-css.ts
@@ -23,7 +23,7 @@ export interface ModifiableCSSRule {
     declarations: ModifiableCSSDeclaration[];
 }
 
-export function getModifiableCSSDeclaration(property: string, value: string, rule: CSSStyleRule, isCancelled: () => boolean): ModifiableCSSDeclaration {
+export function getModifiableCSSDeclaration(property: string, value: string, rule: CSSStyleRule, ignoreImageSelectors: string[], isCancelled: () => boolean): ModifiableCSSDeclaration {
     const important = Boolean(rule && rule.style && rule.style.getPropertyPriority(property));
     const sourceValue = value;
     if (property.startsWith('--')) {
@@ -38,7 +38,7 @@ export function getModifiableCSSDeclaration(property: string, value: string, rul
             return {property, value: modifier, important, sourceValue};
         }
     } else if (property === 'background-image' || property === 'list-style-image') {
-        const modifier = getBgImageModifier(property, value, rule, isCancelled);
+        const modifier = getBgImageModifier(value, rule, ignoreImageSelectors, isCancelled);
         if (modifier) {
             return {property, value: modifier, important, sourceValue};
         }
@@ -233,7 +233,20 @@ const gradientRegex = /[\-a-z]+gradient\(([^\(\)]*(\(([^\(\)]*(\(.*?\)))*[^\(\)]
 const imageDetailsCache = new Map<string, ImageDetails>();
 const awaitingForImageLoading = new Map<string, ((imageDetails: ImageDetails) => void)[]>();
 
-function getBgImageModifier(prop: string, value: string, rule: CSSStyleRule, isCancelled: () => boolean): string | CSSValueModifier {
+function shouldIgnoreImage(element: CSSStyleRule, selectors: string[]) {
+    if (!element) {
+        return false;
+    }
+    for (let i = 0; i < selectors.length; i++) {
+        const ingnoredSelector = selectors[i];
+        if (element.selectorText.match(ingnoredSelector)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+function getBgImageModifier(value: string, rule: CSSStyleRule, ignoreImageSelectors: string[], isCancelled: () => boolean): string | CSSValueModifier {
     try {
         const gradients = getMatches(gradientRegex, value);
         const urls = getMatches(cssURLRegex, value);
@@ -311,6 +324,9 @@ function getBgImageModifier(prop: string, value: string, rule: CSSStyleRule, isC
                     imageDetails = imageDetailsCache.get(url);
                 } else {
                     try {
+                        if (shouldIgnoreImage(rule, ignoreImageSelectors)) {
+                            return null;
+                        }
                         if (awaitingForImageLoading.has(url)) {
                             const awaiters = awaitingForImageLoading.get(url);
                             imageDetails = await new Promise<ImageDetails>((resolve) => awaiters.push(resolve));
diff --git a/src/inject/dynamic-theme/style-manager.ts b/src/inject/dynamic-theme/style-manager.ts
index c81f9b51..28fdc9f0 100644
--- a/src/inject/dynamic-theme/style-manager.ts
+++ b/src/inject/dynamic-theme/style-manager.ts
@@ -31,7 +31,7 @@ export type StyleElement = HTMLLinkElement | HTMLStyleElement;
 
 export interface StyleManager {
     details(): {variables: Map<string, string>};
-    render(theme: Theme, variables: Map<string, string>): void;
+    render(theme: Theme, variables: Map<string, string>, ignoreImageAnalysis: string[]): void;
     pause(): void;
     destroy(): void;
     watch(): void;
@@ -226,7 +226,7 @@ export function manageStyle(element: StyleElement, {update, loadingStart, loadin
 
     let forceRenderStyle = false;
 
-    function render(theme: Theme, variables: Map<string, string>) {
+    function render(theme: Theme, variables: Map<string, string>, ignoreImageAnalysis: string[]) {
         const rules = getRulesSync();
         if (!rules) {
             return;
@@ -276,6 +276,7 @@ export function manageStyle(element: StyleElement, {update, loadingStart, loadin
                 sourceCSSRules: rules,
                 theme,
                 variables,
+                ignoreImageAnalysis,
                 force,
                 isAsyncCancelled: () => cancelAsyncOperations,
             });
diff --git a/src/inject/dynamic-theme/stylesheet-modifier.ts b/src/inject/dynamic-theme/stylesheet-modifier.ts
index 5c845075..d33e872b 100644
--- a/src/inject/dynamic-theme/stylesheet-modifier.ts
+++ b/src/inject/dynamic-theme/stylesheet-modifier.ts
@@ -44,6 +44,7 @@ export function createStyleSheetModifier() {
         sourceCSSRules: CSSRuleList;
         theme: Theme;
         variables: Map<string, string>;
+        ignoreImageAnalysis: string[]
         force: boolean;
         prepareSheet: () => CSSStyleSheet;
         isAsyncCancelled: () => boolean;
@@ -51,7 +52,7 @@ export function createStyleSheetModifier() {
 
     function modifySheet(options: ModifySheetOptions): void {
         const rules = options.sourceCSSRules;
-        const {theme, variables, force, prepareSheet, isAsyncCancelled} = options;
+        const {theme, variables, ignoreImageAnalysis, force, prepareSheet, isAsyncCancelled} = options;
 
         let rulesChanged = (rulesModCache.size === 0);
         const notFoundCacheKeys = new Set(rulesModCache.keys());
@@ -94,7 +95,7 @@ export function createStyleSheetModifier() {
             const modDecs: ModifiableCSSDeclaration[] = [];
             const targetRule = varsRule || rule;
             targetRule && targetRule.style && iterateCSSDeclarations(targetRule.style, (property, value) => {
-                const mod = getModifiableCSSDeclaration(property, value, rule, isAsyncCancelled);
+                const mod = getModifiableCSSDeclaration(property, value, rule, ignoreImageAnalysis, isAsyncCancelled);
                 if (mod) {
                     modDecs.push(mod);
                 }
diff --git a/src/inject/index.ts b/src/inject/index.ts
index 470c9357..c7d9bfc8 100644
--- a/src/inject/index.ts
+++ b/src/inject/index.ts
@@ -3,6 +3,7 @@ import {createOrUpdateSVGFilter, removeSVGFilter} from './svg-filter';
 import {createOrUpdateDynamicTheme, removeDynamicTheme, cleanDynamicThemeCache} from './dynamic-theme';
 import {logInfo, logWarn} from './utils/log';
 import {watchForColorSchemeChange} from './utils/watch-color-scheme';
+import {collectCSS} from './dynamic-theme/css-collection';
 
 function onMessage({type, data}) {
     switch (type) {
@@ -26,6 +27,10 @@ function onMessage({type, data}) {
             createOrUpdateDynamicTheme(filter, fixes, isIFrame);
             break;
         }
+        case 'export-css': {
+            collectCSS().then((collectedCSS) => chrome.runtime.sendMessage({type: 'export-css-response', data: collectedCSS}));
+            break;
+        }
         case 'clean-up': {
             removeStyle();
             removeSVGFilter();
diff --git a/src/ui/popup/manage-settings-page/export-theme.tsx b/src/ui/popup/manage-settings-page/export-theme.tsx
new file mode 100644
index 00000000..737cb16f
--- /dev/null
+++ b/src/ui/popup/manage-settings-page/export-theme.tsx
@@ -0,0 +1,35 @@
+import {m} from 'malevic';
+import {Button} from '../../controls';
+import {saveFile} from '../../utils';
+import ControlGroup from '../control-group';
+import {getURLHost} from '../../../utils/url';
+
+export default function ExportTheme() {
+    const listener = ({type, data}, sender: chrome.runtime.MessageSender) => {
+        if (type === 'export-css-response') {
+            const url = getURLHost(sender.tab.url).replace(/[^a-z0-1\-]/g, '-');
+            saveFile(`DarkReader-${url}.css`, data);
+            chrome.runtime.onMessage.removeListener(listener);
+        }
+    };
+
+    function exportCSS() {
+        chrome.runtime.onMessage.addListener(listener);
+        chrome.runtime.sendMessage({type: 'request-export-css'});
+    }
+    return (
+        <ControlGroup>
+            <ControlGroup.Control>
+                <Button
+                    onclick={exportCSS}
+                    class="settings-button"
+                >
+                    Export Dynamic Theme
+                </Button>
+            </ControlGroup.Control>
+            <ControlGroup.Description>
+                Save generated CSS to a file
+            </ControlGroup.Description>
+        </ControlGroup>
+    );
+}
diff --git a/src/ui/popup/manage-settings-page/index.tsx b/src/ui/popup/manage-settings-page/index.tsx
index 092eb483..9c7159f1 100644
--- a/src/ui/popup/manage-settings-page/index.tsx
+++ b/src/ui/popup/manage-settings-page/index.tsx
@@ -4,14 +4,24 @@ import ResetButtonGroup from './reset-settings-button';
 import ImportButton from './import-settings';
 import ExportButton from './export-settings';
 import SyncSettings from './sync-settings';
+import ExportTheme from './export-theme';
+import {isURLInList} from '../../../utils/url';
+import themeEngines from '../../../generators/theme-engines';
 
 export default function ManageSettingsPage(props: ViewProps) {
+    const custom = props.data.settings.customThemes.find(
+        ({url}) => isURLInList(props.tab.url, url)
+    );
+    const engine = custom ?
+        custom.theme.engine :
+        props.data.settings.theme.engine;
 
     return (
         <section class="m-section">
             <SyncSettings {...props} />
             <ImportButton {...props} />
             <ExportButton {...props} />
+            {engine === themeEngines.dynamicTheme ? <ExportTheme /> : null}
             <ResetButtonGroup {...props} />
         </section>
     );
diff --git a/src/utils/text.ts b/src/utils/text.ts
index 5c39a41d..1c063cac 100644
--- a/src/utils/text.ts
+++ b/src/utils/text.ts
@@ -60,3 +60,47 @@ export function getMatches(regex: RegExp, input: string, group = 0) {
 export function getStringSize(value: string) {
     return value.length * 2;
 }
+
+export function formatCSS(text: string) {
+
+    function trimLeft(text: string) {
+        return text.replace(/^\s+/, '');
+    }
+
+    function getIndent(depth: number) {
+        if (depth === 0) {
+            return '';
+        }
+        return ' '.repeat(4 * depth);
+    }
+
+    const emptyRuleRegexp = /[^{}]+{\s*}/g;
+    while (emptyRuleRegexp.test(text)) {
+        text = text.replace(emptyRuleRegexp, '');
+    }
+
+    const css = (text
+        .replace(/\s{2,}/g, ' ') // Replacing multiple spaces to one
+        .replace(/\{/g, '{\n') // {
+        .replace(/\}/g, '\n}\n') // }
+        .replace(/\;(?![^(\(|\")]*(\)|\"))/g, ';\n') // ; and do not target between () and ""
+        .replace(/\,(?![^(\(|\")]*(\)|\"))/g, ',\n') // , and do not target between () and ""
+        .replace(/\n\s*\n/g, '\n') // Remove \n Without any characters between it to the next \n
+        .split('\n'));
+
+    let depth = 0;
+    const formatted = [];
+
+    for (let x = 0, len = css.length; x < len; x++) {
+        const line = css[x] + '\n';
+        if (line.match(/\{/)) { // {
+            formatted.push(getIndent(depth++) + trimLeft(line));
+        } else if (line.match(/\}/)) { // }
+            formatted.push(getIndent(--depth) + trimLeft(line));
+        } else { // CSS line
+            formatted.push(getIndent(depth) + trimLeft(line));
+        }
+    }
+
+    return formatted.join('').trim();
+}


Test patch:
diff --git a/tests/config/config.tests.ts b/tests/config/config.tests.ts
index 0d4aba2e..12c1bf26 100644
--- a/tests/config/config.tests.ts
+++ b/tests/config/config.tests.ts
@@ -62,7 +62,7 @@ test('Dynamic Theme Fixes config', async () => {
     expect(fixes.map(({url}) => url[0])).toEqual(fixes.map(({url}) => url[0]).sort(compareURLPatterns));
 
     // selectors should have no comma
-    expect(fixes.every(({invert, ignoreInlineStyle}) => (invert || []).concat(ignoreInlineStyle || []).every((s) => s.indexOf(',') < 0))).toBe(true);
+    expect(fixes.every(({invert, ignoreInlineStyle, ignoreImageAnalysis}) => (invert || []).concat(ignoreInlineStyle || []).concat(ignoreImageAnalysis || []).every((s) => s.indexOf(',') < 0))).toBe(true);
 
     // fixes are properly formatted
     expect(throwIfDifferent(file, formatDynamicThemeFixes(fixes), 'Dynamic fixes format error')).not.toThrow();
@@ -84,10 +84,14 @@ test('Dynamic Theme Fixes config', async () => {
         '========',
         'wikipedia.org',
         'IGNORE INLINE STYLE', 'a', 'b',
+        '========',
+        'duckduckgo.com',
+        'IGNORE IMAGE ANALYSIS', 'img[alt="Logo"]', 'canvas',
     ].join('\r\n'))).toEqual([
         {url: ['inbox.google.com', 'mail.google.com'], invert: ['a', 'b'], css: '.x { color: white !important; }'},
         {url: ['twitter.com'], invert: ['c', 'd']},
         {url: ['wikipedia.org'], ignoreInlineStyle: ['a', 'b']},
+        {url: ['duckduckgo.com'], ignoreImageAnalysis: ['img[alt="Logo"]', 'canvas']},
     ]);
 });
 
diff --git a/tests/utils/text.tests.ts b/tests/utils/text.tests.ts
new file mode 100644
index 00000000..d51cfb34
--- /dev/null
+++ b/tests/utils/text.tests.ts
@@ -0,0 +1,101 @@
+import {formatCSS} from '../../src/utils/text';
+
+test('CSS formatting', () => {
+    expect(formatCSS('div { color: red; }'))
+        .toEqual([
+            'div {',
+            '    color: red;',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('div { color: red; } .list-item { background: rgb(0, 0, 0); color: white; }'))
+        .toEqual([
+            'div {',
+            '    color: red;',
+            '}',
+            '.list-item {',
+            '    background: rgb(0, 0, 0);',
+            '    color: white;',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('@media screen { div { color: red; } span { color: red; } } @media all { p { color: green; } }'))
+        .toEqual([
+            '@media screen {',
+            '    div {',
+            '        color: red;',
+            '    }',
+            '    span {',
+            '        color: red;',
+            '    }',
+            '}',
+            '@media all {',
+            '    p {',
+            '        color: green;',
+            '    }',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('div, span { background: green; color: red; }'))
+        .toEqual([
+            'div,',
+            'span {',
+            '    background: green;',
+            '    color: red;',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('@media print, screen and (min-width: 20rem) { div, span { background: green; color: red; } }'))
+        .toEqual([
+            '@media print,',
+            'screen and (min-width: 20rem) {',
+            '    div,',
+            '    span {',
+            '        background: green;',
+            '        color: red;',
+            '    }',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('.icon { background-image: url(data:image/gif;base64,XYZ); }'))
+        .toEqual([
+            '.icon {',
+            '    background-image: url(data:image/gif;base64,XYZ);',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('img[src*="a,b;c"] { filter: invert(1); }'))
+        .toEqual([
+            'img[src*="a,b;c"] {',
+            '    filter: invert(1);',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('img[src*="a,b;c"] {\n    filter: invert(1); \n}'))
+        .toEqual([
+            'img[src*="a,b;c"] {',
+            '    filter: invert(1);',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('div { } span { color: red; } button { }'))
+        .toEqual([
+            'span {',
+            '    color: red;',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('@media all { div { } span { color: red; } } @media all { div { } } @media all { button { color: green; } }'))
+        .toEqual([
+            '@media all {',
+            '    span {',
+            '        color: red;',
+            '    }',
+            '}',
+            '@media all {',
+            '    button {',
+            '        color: green;',
+            '    }',
+            '}',
+        ].join('\n'));
+});