Repository: darkreader/darkreader (ts)

Issue:
[BUG] negative z-index does not work with dark reader
<!--
Thank you for taking the time to report a bug.
Please make sure there is no existing issue about this kind of bug.
-->

### **Describe the bug**

The dark reader extension makes sites with z-index<0 not show the element with that attribute, e.g. https://projectstream.it/ (the div with class name: "video-container" is replaced by a dark grey background.

### **Steps to reproduce**

- Go to https://projectstream.it/
- A background video should appear'
- It does not appear with the extension activated because of the negative z-index of the div


Pull request:
Fix for projectstream.it
- Remove background-color
- Resolve #3047

Patch:
diff --git a/package.json b/package.json
index e59e3a69..b65f6657 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,13 @@
     "debug-watch": "node tasks/build.js --debug --watch",
     "prepublishOnly": "npm test && npm run api",
     "release": "npm test && node tasks/build.js --release",
-    "test": "jest --config=tests/jest.config.js",
-    "test:inject": "jest --config=tests/inject/jest.config.js"
+    "test": "npm run test:config && npm run test:inject && npm run test:utils",
+    "test:config": "jest --config=tests/config/jest.config.js",
+    "test:config:debug": "node --inspect-brk ./node_modules/jest/bin/jest --config=tests/config/jest.config.js --runInBand --no-cache --watch",
+    "test:inject": "jest --config=tests/inject/jest.config.js",
+    "test:inject:debug": "node --inspect-brk ./node_modules/jest/bin/jest --config=tests/inject/jest.config.js --runInBand --no-cache --watch",
+    "test:utils": "jest --config=tests/utils/jest.config.js",
+    "test:utils:debug": "node --inspect-brk ./node_modules/jest/bin/jest --config=tests/utils/jest.config.js --runInBand --no-cache --watch"
   },
   "main": "darkreader.js",
   "repository": {
diff --git a/src/config/dynamic-theme-fixes.config b/src/config/dynamic-theme-fixes.config
index 9d8d843a..a91eca60 100644
--- a/src/config/dynamic-theme-fixes.config
+++ b/src/config/dynamic-theme-fixes.config
@@ -946,6 +946,15 @@ INVERT
 
 ================================
 
+dennisbareis.com
+
+CSS
+body {
+    background-image: none !important;
+}
+
+================================
+
 deno.land
 
 INVERT
@@ -1732,6 +1741,18 @@ CSS
 
 ================================
 
+git-scm.com
+
+INVERT
+img[alt="Git"]
+
+CSS
+body, #masthead {
+    background-image: none !important;
+}
+
+================================
+
 github.blog
 
 INVERT
@@ -3498,6 +3519,15 @@ INVERT
 
 ================================
 
+projectstream.it
+
+CSS
+body {
+    background: none !important;
+}
+
+================================
+
 prostovpn.org
 
 CSS
diff --git a/src/inject/dynamic-theme/css-rules.ts b/src/inject/dynamic-theme/css-rules.ts
index e4794a2c..7f2f0882 100644
--- a/src/inject/dynamic-theme/css-rules.ts
+++ b/src/inject/dynamic-theme/css-rules.ts
@@ -68,7 +68,7 @@ export function getCSSURLValue(cssURL: string) {
 
 export function getCSSBaseBath(url: string) {
     const cssURL = parseURL(url);
-    return `${cssURL.protocol}//${cssURL.host}${cssURL.pathname.replace(/\?.*$/, '').replace(/(\/)([^\/]+)$/i, '$1')}`;
+    return `${cssURL.origin}${cssURL.pathname.replace(/\?.*$/, '').replace(/(\/)([^\/]+)$/i, '$1')}`;
 }
 
 export function replaceCSSRelativeURLsWithAbsolute($css: string, cssBasePath: string) {
@@ -92,11 +92,28 @@ export function replaceCSSFontFace($css: string) {
 
 const varRegex = /var\((--[^\s,\(\)]+),?\s*([^\(\)]*(\([^\(\)]*\)[^\(\)]*)*\s*)\)/g;
 
-export function replaceCSSVariables(value: string, variables: Map<string, string>) {
+export function replaceCSSVariables(
+    value: string,
+    variables: Map<string, string>,
+    stack = new Set<string>(),
+) {
     let missing = false;
+    const unresolvable = new Set<string>();
     const result = value.replace(varRegex, (match, name, fallback) => {
+        if (stack.has(name)) {
+            logWarn(`Circular reference to variable ${name}`);
+            if (fallback) {
+                return fallback;
+            }
+            missing = true;
+            return match;
+        }
         if (variables.has(name)) {
-            return variables.get(name);
+            const value = variables.get(name);
+            if (value.match(varRegex)) {
+                unresolvable.add(name);
+            }
+            return value;
         } else if (fallback) {
             return fallback;
         } else {
@@ -109,7 +126,8 @@ export function replaceCSSVariables(value: string, variables: Map<string, string
         return result;
     }
     if (result.match(varRegex)) {
-        return replaceCSSVariables(result, variables);
+        unresolvable.forEach((v) => stack.add(v));
+        return replaceCSSVariables(result, variables, stack);
     }
     return result;
 }
diff --git a/src/inject/dynamic-theme/url.ts b/src/inject/dynamic-theme/url.ts
index a442fcd5..1596c402 100644
--- a/src/inject/dynamic-theme/url.ts
+++ b/src/inject/dynamic-theme/url.ts
@@ -1,41 +1,24 @@
-export function parseURL(url: string) {
+function fixBaseURL($url: string) {
     const a = document.createElement('a');
-    a.href = url;
-    return a;
+    a.href = $url;
+    return a.href;
 }
 
-function backwards($base: string, $relative: string) {
-    const b = parseURL($base);
-    let pathParts = b.pathname.split('/');
-    pathParts = pathParts.concat(...$relative.split('/')).filter((p) => p);
-    let backwardIndex: number;
-    while ((backwardIndex = pathParts.indexOf('..')) > 0) {
-        pathParts.splice(backwardIndex - 1, 2);
+export function parseURL($url: string, $base: string = null) {
+    if ($base) {
+        $base = fixBaseURL($base);
+        return new URL($url, $base);
     }
-    return pathParts;
+    $url = fixBaseURL($url);
+    return new URL($url);
 }
 
 export function getAbsoluteURL($base: string, $relative: string) {
-    if ($relative.match(/^.*?\/\//) || $relative.match(/^data\:/)) {
-        if ($relative.startsWith('//')) {
-            if ($relative.includes('..')) {
-                return `${location.protocol}${backwards($base, $relative).join('/')}`;
-            }
-            return `${location.protocol}${$relative}`;
-        }
+    if ($relative.match(/^data\:/)) {
         return $relative;
     }
+
     const b = parseURL($base);
-    if ($relative.startsWith('/')) {
-        const u = parseURL(`${b.protocol}//${b.host}${$relative}`);
-        return u.href;
-    }
-    let pathParts = b.pathname.split('/');
-    const lastPathPart = pathParts[pathParts.length - 1];
-    if (lastPathPart.match(/\.[a-z]+$/i)) {
-        pathParts.pop();
-    }
-    pathParts = backwards($base, $relative);
-    const u = parseURL(`${b.protocol}//${b.host}/${pathParts.join('/')}`);
-    return u.href;
+    const a = parseURL($relative, b.href);
+    return a.href;
 }


Test patch:
diff --git a/tests/config.tests.ts b/tests/config/config.tests.ts
similarity index 91%
rename from tests/config.tests.ts
rename to tests/config/config.tests.ts
index 36aa7548..0d4aba2e 100644
--- a/tests/config.tests.ts
+++ b/tests/config/config.tests.ts
@@ -1,14 +1,14 @@
 import {readFile} from 'fs';
 import {resolve as resolvePath} from 'path';
-import {compareURLPatterns} from '../src/utils/url';
-import {parseArray, formatArray, getTextDiffIndex, getTextPositionMessage} from '../src/utils/text';
-import {parseInversionFixes, formatInversionFixes} from '../src/generators/css-filter';
-import {parseDynamicThemeFixes, formatDynamicThemeFixes} from '../src/generators/dynamic-theme';
-import {parseStaticThemes, formatStaticThemes} from '../src/generators/static-theme';
+import {compareURLPatterns} from '../../src/utils/url';
+import {parseArray, formatArray, getTextDiffIndex, getTextPositionMessage} from '../../src/utils/text';
+import {parseInversionFixes, formatInversionFixes} from '../../src/generators/css-filter';
+import {parseDynamicThemeFixes, formatDynamicThemeFixes} from '../../src/generators/dynamic-theme';
+import {parseStaticThemes, formatStaticThemes} from '../../src/generators/static-theme';
 
 function readConfig(fileName) {
     return new Promise<string>((resolve, reject) => {
-        readFile(resolvePath(__dirname, '../src/config/', fileName), {encoding: 'utf-8'}, (err, data) => {
+        readFile(resolvePath(__dirname, '../../src/config/', fileName), {encoding: 'utf-8'}, (err, data) => {
             if (err) {
                 reject(err);
                 return;
diff --git a/tests/config/jest.config.js b/tests/config/jest.config.js
new file mode 100644
index 00000000..1d1d1e63
--- /dev/null
+++ b/tests/config/jest.config.js
@@ -0,0 +1,26 @@
+module.exports = {
+    verbose: true,
+    testEnvironment: 'node',
+    transform: {
+        '^.+\\.ts(x?)$': 'ts-jest'
+    },
+    testRegex: 'tests/config/.*\\.tests\\.ts(x?)$',
+    moduleFileExtensions: [
+        'ts',
+        'tsx',
+        'js'
+    ],
+    collectCoverage: false,
+    coverageDirectory: 'coverage',
+    collectCoverageFrom: [
+        'src/**/*.{ts,tsx}'
+    ],
+    coveragePathIgnorePatterns: [
+        '^.+\\.d\\.ts$'
+    ],
+    globals: {
+        'ts-jest': {
+            tsConfig: './tests/config/tsconfig.json'
+        }
+    }
+};
diff --git a/tests/locales.tests.ts b/tests/config/locales.tests.ts
similarity index 92%
rename from tests/locales.tests.ts
rename to tests/config/locales.tests.ts
index dbb9fad6..7bb32355 100644
--- a/tests/locales.tests.ts
+++ b/tests/config/locales.tests.ts
@@ -15,7 +15,7 @@ function readDir(dir) {
 
 function readLocale(name) {
     return new Promise<string>((resolve, reject) => {
-        fsReadFile(resolvePath(__dirname, '../src/_locales/', name), {encoding: 'utf-8'}, (err, data) => {
+        fsReadFile(resolvePath(__dirname, '../../src/_locales/', name), {encoding: 'utf-8'}, (err, data) => {
             if (err) {
                 reject(err);
                 return;
@@ -26,7 +26,7 @@ function readLocale(name) {
 }
 
 test('Locales', async () => {
-    const files = await readDir('../src/_locales');
+    const files = await readDir('../../src/_locales');
     const enLocale = await readLocale('en.config');
     const enLines = enLocale.split('\n');
     const locales: string[] = [];
diff --git a/tests/tsconfig.json b/tests/config/tsconfig.json
similarity index 80%
rename from tests/tsconfig.json
rename to tests/config/tsconfig.json
index 2d65ac96..8577f299 100644
--- a/tests/tsconfig.json
+++ b/tests/config/tsconfig.json
@@ -1,5 +1,5 @@
 {
-    "extends": "../src/tsconfig",
+    "extends": "../../src/tsconfig",
     "compilerOptions": {
         "module": "commonjs",
         "outDir": "",
diff --git a/tests/inject/dynamic.tests.ts b/tests/inject/dynamic.tests.ts
index 45fcc458..e855aaa5 100644
--- a/tests/inject/dynamic.tests.ts
+++ b/tests/inject/dynamic.tests.ts
@@ -1,10 +1,97 @@
 import {getAbsoluteURL} from '../../src/inject/dynamic-theme/url';
+import {replaceCSSVariables} from '../../src/inject/dynamic-theme/css-rules';
 
 test('Absolute URL', () => {
+    expect(getAbsoluteURL('https://www.google.com', 'image.jpg')).toBe('https://www.google.com/image.jpg');
     expect(getAbsoluteURL('https://www.google.com', '/image.jpg')).toBe('https://www.google.com/image.jpg');
     expect(getAbsoluteURL('https://www.google.com/path', '/image.jpg')).toBe('https://www.google.com/image.jpg');
-    expect(getAbsoluteURL('https://www.google.com/path', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');
-    expect(getAbsoluteURL('https://www.google.com/long/path', '../image.jpg')).toBe('https://www.google.com/long/image.jpg');
+    expect(getAbsoluteURL('//www.google.com', '/image.jpg')).toBe(`${location.protocol}//www.google.com/image.jpg`);
+    expect(getAbsoluteURL('https://www.google.com', 'image.jpg?size=128')).toBe('https://www.google.com/image.jpg?size=128');
+    expect(getAbsoluteURL('https://www.google.com/path', 'image.jpg')).toBe('https://www.google.com/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com/path/', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com/long/path', '../image.jpg')).toBe('https://www.google.com/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com/long/path/', '../image.jpg')).toBe('https://www.google.com/long/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com/long/path/', '../another/image.jpg')).toBe('https://www.google.com/long/another/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com/path/page.html', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com/path/page.html', '/image.jpg')).toBe('https://www.google.com/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com', '//www.google.com/path/image.jpg')).toBe('https://www.google.com/path/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com', '//www.google.com/path/../another/image.jpg')).toBe('https://www.google.com/another/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com', 'https://www.google.com/path/image.jpg')).toBe('https://www.google.com/path/image.jpg');
+    expect(getAbsoluteURL('https://www.google.com', 'https://www.google.com/path/../another/image.jpg')).toBe('https://www.google.com/another/image.jpg');
     expect(getAbsoluteURL('https://www.google.com/path/page.html', 'image.jpg')).toBe('https://www.google.com/path/image.jpg');
     expect(getAbsoluteURL('https://www.google.com/path/page.html', '../image.jpg')).toBe('https://www.google.com/image.jpg');
+    expect(getAbsoluteURL('path/index.html', 'image.jpg')).toBe(`${location.origin}/path/image.jpg`);
+    expect(getAbsoluteURL('path/index.html', '/image.jpg?size=128')).toBe(`${location.origin}/image.jpg?size=128`);
+});
+
+test('Replace CSS variables', () => {
+    expect(
+        replaceCSSVariables(
+            'body { background: var(--bg); color: var(--text); }',
+            new Map(),
+        )
+    ).toBe('body { background: var(--bg); color: var(--text); }');
+
+    expect(
+        replaceCSSVariables(
+            'body { background: var(--bg); color: var(--text); } h1 { color: var(--text); }',
+            new Map([['--bg', 'white'], ['--text', 'black']]),
+        )
+    ).toBe('body { background: white; color: black; } h1 { color: black; }');
+
+    expect(
+        replaceCSSVariables(
+            'body { color: var(--text, red); }',
+            new Map([['--text', 'black']]),
+        )
+    ).toBe('body { color: black; }');
+
+    expect(
+        replaceCSSVariables(
+            'body { color: var(--text, red); }',
+            new Map(),
+        )
+    ).toBe('body { color: red; }');
+
+    expect(
+        replaceCSSVariables(
+            'body { color: var(--text, var(--alert)); }',
+            new Map([['--alert', 'red']]),
+        )
+    ).toBe('body { color: red; }');
+
+    expect(
+        replaceCSSVariables(
+            'body { color: var(--text, var(--alert, green)); }',
+            new Map(),
+        )
+    ).toBe('body { color: green; }');
+
+    expect(
+        replaceCSSVariables(
+            'body { color: var(--text); }',
+            new Map([['--text', 'var(--text)']]),
+        )
+    ).toBe('body { color: var(--text); }');
+
+    expect(
+        replaceCSSVariables(
+            'body { color: var(--text); }',
+            new Map([['--text', 'var(--alert)'], ['--alert', 'var(--text)']]),
+        )
+    ).toBe('body { color: var(--text); }');
+
+    expect(
+        replaceCSSVariables(
+            'body { color: var(--text, var(--alert)); } h1 { color: var(--text); }',
+            new Map([['--text', 'var(--alert)'], ['--alert', 'var(--text)']]),
+        )
+    ).toBe('body { color: var(--text); } h1 { color: var(--text); }');
+
+    expect(
+        replaceCSSVariables(
+            'body { color: var(--text); } h1 { color: var(--alert); }',
+            new Map([['--text', 'var(--alert)'], ['--alert', 'red']]),
+        )
+    ).toBe('body { color: red; } h1 { color: red; }');
 });
diff --git a/tests/inject/jest.config.js b/tests/inject/jest.config.js
index bb1ff3e4..3b1f2d7b 100644
--- a/tests/inject/jest.config.js
+++ b/tests/inject/jest.config.js
@@ -21,6 +21,7 @@ module.exports = {
     globals: {
         'ts-jest': {
             tsConfig: './tests/inject/tsconfig.json'
-        }
+        },
+        __DEBUG__: false,
     }
 };
diff --git a/tests/color.tests.ts b/tests/utils/color.tests.ts
similarity index 99%
rename from tests/color.tests.ts
rename to tests/utils/color.tests.ts
index 1a741aad..01813f1d 100644
--- a/tests/color.tests.ts
+++ b/tests/utils/color.tests.ts
@@ -1,4 +1,4 @@
-import {parse, hslToRGB, rgbToHSL, rgbToString, rgbToHexString, hslToString, HSLA} from '../src/utils/color';
+import {parse, hslToRGB, rgbToHSL, rgbToString, rgbToHexString, hslToString, HSLA} from '../../src/utils/color';
 
 test('Color parsing', () => {
     expect(parse('rgb(255,0,153)')).toEqual({r: 255, g: 0, b: 153, a: 1});
diff --git a/tests/jest.config.js b/tests/utils/jest.config.js
similarity index 81%
rename from tests/jest.config.js
rename to tests/utils/jest.config.js
index 5482690a..8f5db0f8 100644
--- a/tests/jest.config.js
+++ b/tests/utils/jest.config.js
@@ -4,7 +4,7 @@ module.exports = {
     transform: {
         '^.+\\.ts(x?)$': 'ts-jest'
     },
-    testRegex: 'tests/[^/]*\\.tests\\.ts(x?)$',
+    testRegex: 'tests/utils/.*\\.tests\\.ts(x?)$',
     moduleFileExtensions: [
         'ts',
         'tsx',
@@ -20,7 +20,7 @@ module.exports = {
     ],
     globals: {
         'ts-jest': {
-            tsConfig: './tests/tsconfig.json'
+            tsConfig: './tests/utils/tsconfig.json'
         }
     }
 };
diff --git a/tests/time.tests.ts b/tests/utils/time.tests.ts
similarity index 98%
rename from tests/time.tests.ts
rename to tests/utils/time.tests.ts
index ff1c21bc..84d4a44d 100644
--- a/tests/time.tests.ts
+++ b/tests/utils/time.tests.ts
@@ -1,4 +1,4 @@
-import {isInTimeInterval, isNightAtLocation, parseTime, getDuration} from '../src/utils/time';
+import {isInTimeInterval, isNightAtLocation, parseTime, getDuration} from '../../src/utils/time';
 
 test('Time interval', () => {
     expect(isInTimeInterval(new Date(2018, 11, 4, 10), '9:00', '12:00')).toBe(true);
diff --git a/tests/utils/tsconfig.json b/tests/utils/tsconfig.json
new file mode 100644
index 00000000..8577f299
--- /dev/null
+++ b/tests/utils/tsconfig.json
@@ -0,0 +1,11 @@
+{
+    "extends": "../../src/tsconfig",
+    "compilerOptions": {
+        "module": "commonjs",
+        "outDir": "",
+        "types": [
+            "jest",
+            "node"
+        ]
+    }
+}
diff --git a/tests/url.tests.ts b/tests/utils/url.tests.ts
similarity index 98%
rename from tests/url.tests.ts
rename to tests/utils/url.tests.ts
index 686de5a0..bc627c9e 100644
--- a/tests/url.tests.ts
+++ b/tests/utils/url.tests.ts
@@ -1,5 +1,5 @@
-import {isURLEnabled, isURLMatched, isPDF} from '../src/utils/url';
-import {UserSettings} from '../src/definitions';
+import {isURLEnabled, isURLMatched, isPDF} from '../../src/utils/url';
+import {UserSettings} from '../../src/definitions';
 
 test('URL is enabled', () => {
     // Not invert listed
@@ -132,7 +132,7 @@ test('URL is enabled', () => {
     )).toBe(false);
     expect(isPDF(
         'https://upload.wikimedia.org/wikipedia/commons/5/56/ExtIPA_chart_(2015).pdf'
-    )).toBe(true);  
+    )).toBe(true);
 
     // IPV6 Testing
     expect(isURLEnabled(