├── .gitmodules ├── .prettierrc.yaml ├── .gitattributes ├── test ├── test-pages │ ├── stylesheets │ │ ├── foreign.css │ │ ├── local.css │ │ └── index.html │ ├── target-blank │ │ ├── style.css │ │ ├── 1st.html │ │ └── 2nd.html │ ├── invert-test │ │ ├── B.png │ │ ├── C.png │ │ ├── G.png │ │ ├── M.png │ │ ├── R.png │ │ ├── Y.png │ │ ├── gradient.png │ │ └── index.html │ ├── service-worker │ │ ├── 8080 │ │ │ ├── style.css │ │ │ ├── index.js │ │ │ ├── sw.js │ │ │ └── index.html │ │ ├── 8081 │ │ │ └── style.css │ │ └── serve.js │ ├── cors-stylesheets │ │ ├── 8080 │ │ │ ├── linked-same-origin.css │ │ │ └── index.html │ │ ├── 8081 │ │ │ ├── IndieFlower.woff2 │ │ │ ├── cross-origin-import-from-same-origin-linked.css │ │ │ ├── cross-origin-stylesheet-with-cross-origin-import.css │ │ │ └── imported-from-inline.css │ │ ├── 8082 │ │ │ └── cross-origin-stylesheet.css │ │ ├── 8083 │ │ │ └── imported-from-cross-origin.css │ │ └── serve.js │ ├── iframes │ │ ├── iframe.html │ │ └── index.html │ ├── csp-strict │ │ ├── webroot │ │ │ └── index.html │ │ └── serve.js │ ├── style-attr │ │ └── index.html │ ├── background-repeat │ │ └── index.html │ ├── css-var │ │ └── index.html │ ├── css-flexbox │ │ └── index.html │ ├── input-color │ │ └── index.html │ ├── canvas │ │ └── index.html │ ├── simple-node-server.js │ └── inline-style │ │ └── index.html ├── test-method-keys.ts ├── test-utils.js ├── test-render-valid-css.ts ├── test-background-lib.ts └── test-generate-urls.ts ├── .mocharc.json ├── icons ├── icon16.png ├── icon24.png ├── icon32.png ├── icon48.png └── icon64.png ├── README.md ├── .gitignore ├── src ├── types │ └── exportFunction.d.ts ├── preferences │ ├── main.ts │ ├── App.svelte │ └── ColorInput.svelte ├── common │ ├── smart-generate-urls.ts │ ├── get_acceptable_range.ts │ ├── ui-style.ts │ ├── types.ts │ ├── color_utils.ts │ ├── generate-urls.ts │ └── shared.ts ├── methods │ ├── methods-with-executors.ts │ ├── executors │ │ └── invert.ts │ ├── stylesheets │ │ ├── stylesheet-processor.ts │ │ ├── simple-css.ts │ │ ├── invert.ts │ │ └── base.ts │ ├── methods-with-stylesheets.ts │ └── methods.ts ├── background │ ├── lib.ts │ └── index.ts ├── content │ └── index.ts └── browser-action │ └── index.ts ├── tsconfig.json ├── coverage-relative-json-summary.js ├── check-coverage ├── package.json ├── manifest.json ├── .pre-commit-config.yaml ├── rollup.config.mjs ├── ui ├── internal.css ├── configure-for-current-tab-panel.html ├── preferences.html └── bootstrap.css ├── .eslintrc.js ├── last-coverage-summary.json └── LICENSE /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: all 2 | singleQuote: true 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | last-coverage-summary.json -merge 2 | package-lock.json -merge -diff 3 | -------------------------------------------------------------------------------- /test/test-pages/stylesheets/foreign.css: -------------------------------------------------------------------------------- 1 | #test-span2 { 2 | color: #00ff00 !important; 3 | } 4 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "test/*.ts", 4 | "require": "ts-node/register" 5 | } 6 | -------------------------------------------------------------------------------- /icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/icons/icon16.png -------------------------------------------------------------------------------- /icons/icon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/icons/icon24.png -------------------------------------------------------------------------------- /icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/icons/icon32.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/icons/icon48.png -------------------------------------------------------------------------------- /icons/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/icons/icon64.png -------------------------------------------------------------------------------- /test/test-pages/target-blank/style.css: -------------------------------------------------------------------------------- 1 | .yellow-on-blue { 2 | color: #707000; 3 | background-color: #c0c0ff; 4 | } 5 | -------------------------------------------------------------------------------- /test/test-pages/invert-test/B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/test/test-pages/invert-test/B.png -------------------------------------------------------------------------------- /test/test-pages/invert-test/C.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/test/test-pages/invert-test/C.png -------------------------------------------------------------------------------- /test/test-pages/invert-test/G.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/test/test-pages/invert-test/G.png -------------------------------------------------------------------------------- /test/test-pages/invert-test/M.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/test/test-pages/invert-test/M.png -------------------------------------------------------------------------------- /test/test-pages/invert-test/R.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/test/test-pages/invert-test/R.png -------------------------------------------------------------------------------- /test/test-pages/invert-test/Y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/test/test-pages/invert-test/Y.png -------------------------------------------------------------------------------- /test/test-pages/stylesheets/local.css: -------------------------------------------------------------------------------- 1 | #test-span1 { 2 | color: blue !important; 3 | background: red url('http://trollface_small.jpg.to/'); 4 | } 5 | -------------------------------------------------------------------------------- /test/test-pages/invert-test/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/test/test-pages/invert-test/gradient.png -------------------------------------------------------------------------------- /test/test-pages/service-worker/8080/style.css: -------------------------------------------------------------------------------- 1 | #same-origin .ok { 2 | display: initial !important; 3 | } 4 | #same-origin .fail { 5 | display: none !important; 6 | } 7 | -------------------------------------------------------------------------------- /test/test-pages/service-worker/8081/style.css: -------------------------------------------------------------------------------- 1 | #cross-origin .ok { 2 | display: initial !important; 3 | } 4 | #cross-origin .fail { 5 | display: none !important; 6 | } 7 | -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/8081/IndieFlower.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-khvoinitsky/dark-background-light-text-extension/HEAD/test/test-pages/cors-stylesheets/8081/IndieFlower.woff2 -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/8082/cross-origin-stylesheet.css: -------------------------------------------------------------------------------- 1 | #cross-origin-stylesheet { 2 | color: #007000; 3 | background-color: #a0a0ff; 4 | background-image: url('icon.svg'); 5 | } 6 | -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/8083/imported-from-cross-origin.css: -------------------------------------------------------------------------------- 1 | #imported-from-cross-origin { 2 | color: #007000; 3 | background-color: #a0a0ff; 4 | background-image: url('icon.svg'); 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dark Background and Light Text 2 | 3 | WebExtension that turns each page colors into light text on dark background. 4 | 5 | https://addons.mozilla.org/firefox/addon/dark-background-light-text/ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .directory 2 | .idea 3 | *.xpi 4 | .DS_Store 5 | node_modules 6 | web-ext-artifacts 7 | package-lock.json 8 | !/package-lock.json 9 | .log 10 | dist 11 | *~ 12 | .#* 13 | *# 14 | .nyc_output/ 15 | coverage/ 16 | -------------------------------------------------------------------------------- /src/types/exportFunction.d.ts: -------------------------------------------------------------------------------- 1 | export function exportFunction( 2 | func: Function, 3 | targetScope: object, 4 | options?: { 5 | defineAs?: string; 6 | allowCrossOriginArguments?: boolean; 7 | }, 8 | ): Function; 9 | -------------------------------------------------------------------------------- /test/test-pages/iframes/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | test iframe 4 | 5 | 6 | 7 | 8 | iframe content 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/8081/cross-origin-import-from-same-origin-linked.css: -------------------------------------------------------------------------------- 1 | #cross-origin-import-from-same-origin-linked { 2 | color: #007000; 3 | background-color: #a0a0ff; 4 | background-image: url('icon.svg'); 5 | } 6 | -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/8080/linked-same-origin.css: -------------------------------------------------------------------------------- 1 | @import 'http://[::1]:8081/cross-origin-import-from-same-origin-linked.css'; 2 | 3 | #linked-same-origin { 4 | color: #007000; 5 | background-color: #a0a0ff; 6 | background-image: url('icon.svg'); 7 | } 8 | -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/8081/cross-origin-stylesheet-with-cross-origin-import.css: -------------------------------------------------------------------------------- 1 | @import 'http://[::1]:8083/imported-from-cross-origin.css' (min-width: 600px); 2 | 3 | #cross-origin-stylesheet-with-cross-origin-import { 4 | color: #007000; 5 | background-color: #a0a0ff; 6 | background-image: url('icon.svg'); 7 | } 8 | -------------------------------------------------------------------------------- /test/test-pages/csp-strict/webroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSP self 6 | 11 | 12 | 13 |
Red text
14 | 15 | 16 | -------------------------------------------------------------------------------- /test/test-pages/service-worker/serve.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const { serve } = require('../simple-node-server'); 4 | 5 | const ports = [8080, 8081]; 6 | 7 | ports.forEach((port) => 8 | serve({ 9 | port, 10 | // eslint-disable-next-line no-unused-vars 11 | mutateFilePath: ({ filePath, request, bind_address }) => 12 | `./${port}${filePath}`, 13 | }), 14 | ); 15 | -------------------------------------------------------------------------------- /test/test-pages/style-attr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | style attr 6 | 7 | 8 | color: #303000
9 | color: #303000; background-color: #A0FFA0 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/test-method-keys.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { describe } from 'mocha'; 3 | import { methods } from '../src/methods/methods'; 4 | 5 | describe('Ensure method IDs are consistent', () => { 6 | Object.entries(methods).forEach(([key, val]) => { 7 | it(val.label, () => { 8 | assert( 9 | key === val.number, 10 | `${val.label} key (${key}) does not match its number (${val.number})`, 11 | ); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/preferences/main.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore: suppress '(!) Plugin typescript: @rollup/plugin-typescript TS2307: Cannot find module './App.svelte' or its corresponding type declarations.' error for non-svelte bundles compilation 2 | import App from './App.svelte'; 3 | import type { Browser } from 'webextension-polyfill'; 4 | declare const browser: Browser; 5 | 6 | const app = new App({ 7 | target: document.body, 8 | props: { 9 | browser: browser, 10 | }, 11 | }); 12 | 13 | export default app; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "importsNotUsedAsValues": "error", 7 | "isolatedModules": false, 8 | "sourceMap": true, 9 | "strict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "noImplicitAny": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "forceConsistentCasingInFileNames": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/test-pages/csp-strict/serve.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const { serve } = require('../simple-node-server'); 4 | 5 | serve({ 6 | port: 8080, 7 | // eslint-disable-next-line no-unused-vars 8 | mutateHeaders({ headers, request }) { 9 | // eslint-disable-next-line no-param-reassign 10 | headers['Content-Security-Policy'] = "default-src 'self'"; 11 | return headers; 12 | }, 13 | // eslint-disable-next-line no-unused-vars 14 | mutateFilePath: ({ filePath, request, bind_address }) => 15 | `./webroot${filePath}`, 16 | }); 17 | -------------------------------------------------------------------------------- /test/test-pages/target-blank/1st.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 1st target-blank 6 | 7 | 8 | 9 |

1st

10 | 2nd target="_blank"
11 | 2nd
12 | Some not styled text
13 | Some yellow on blue styled text
14 | 15 | 16 | -------------------------------------------------------------------------------- /test/test-pages/target-blank/2nd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2nd target-blank 6 | 7 | 8 | 9 |

2nd

10 | 1st target="_blank"
11 | 1st
12 | Some not styled text
13 | Some yellow on blue styled text
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/common/smart-generate-urls.ts: -------------------------------------------------------------------------------- 1 | import { parse as tldts_parse } from 'tldts'; 2 | import { generate_urls } from './generate-urls'; 3 | 4 | export function smart_generate_urls( 5 | url_str: string, 6 | hint: boolean = false, 7 | ): string[] { 8 | return generate_urls(url_str, hint, (hostname) => { 9 | const tldts_obj = tldts_parse(hostname, { 10 | detectIp: false, 11 | extractHostname: false, 12 | }); 13 | return [ 14 | tldts_obj.domain!, 15 | tldts_obj.subdomain ? tldts_obj.subdomain.split('.') : [], 16 | ]; 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/serve.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const { serve } = require('../simple-node-server'); 4 | 5 | const ports = [8080, 8081, 8082, 8083]; 6 | 7 | ports.forEach((port) => 8 | serve({ 9 | port, 10 | mutateHeaders({ headers, request }) { 11 | if (request.url.endsWith('.woff2')) { 12 | // eslint-disable-next-line no-param-reassign 13 | headers['Access-Control-Allow-Origin'] = '*'; 14 | } 15 | return headers; 16 | }, 17 | // eslint-disable-next-line no-unused-vars 18 | mutateFilePath: ({ filePath, request, bind_address }) => 19 | `./${port}${filePath}`, 20 | }), 21 | ); 22 | -------------------------------------------------------------------------------- /test/test-pages/service-worker/8080/index.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | window.addEventListener('load', () => { 3 | navigator.serviceWorker.register('/sw.js').then( 4 | (registration) => { 5 | // Registration was successful 6 | console.log( 7 | 'ServiceWorker registration successful with scope: ', 8 | registration.scope, 9 | ); 10 | }, 11 | (err) => { 12 | // registration failed :( 13 | console.log('ServiceWorker registration failed: ', err); 14 | }, 15 | ); 16 | }); 17 | } else { 18 | console.log('serviceWorker is not available in your browser.'); 19 | } 20 | -------------------------------------------------------------------------------- /coverage-relative-json-summary.js: -------------------------------------------------------------------------------- 1 | const { ReportBase } = require('istanbul-lib-report'); 2 | const { writeFileSync } = require('fs'); 3 | const { relative } = require('path'); 4 | 5 | class RelativeJsonSummaryReport extends ReportBase { 6 | constructor() { 7 | super(); 8 | 9 | this.data = {}; 10 | } 11 | 12 | onDetail(node) { 13 | this.data[relative(process.cwd(), node.getFileCoverage().path)] = 14 | node.getCoverageSummary(); 15 | } 16 | 17 | onEnd() { 18 | writeFileSync( 19 | 'last-coverage-summary.json', 20 | `${JSON.stringify(this.data, null, 2)}\n`, 21 | { encoding: 'utf8' }, 22 | ); 23 | } 24 | } 25 | module.exports = RelativeJsonSummaryReport; 26 | -------------------------------------------------------------------------------- /src/methods/methods-with-executors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | methods as methods_bare, 3 | STYLESHEET_PROCESSOR_ID, 4 | INVERT_ID, 5 | } from './methods'; 6 | import type { MethodsMetadataWithExecutors } from '../common/types'; 7 | import { StylesheetColorProcessor } from './executors/stylesheet-color-processor'; 8 | import { InvertMethod } from './executors/invert'; 9 | 10 | const methods = methods_bare as MethodsMetadataWithExecutors; 11 | 12 | methods[STYLESHEET_PROCESSOR_ID].executor = StylesheetColorProcessor; 13 | methods[INVERT_ID].executor = InvertMethod; 14 | 15 | for (const k of Object.keys(methods)) { 16 | if (k !== STYLESHEET_PROCESSOR_ID && k !== INVERT_ID) { 17 | methods[k].executor = null; 18 | } 19 | } 20 | 21 | export { methods }; 22 | -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/8081/imported-from-inline.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'umgsfdxynf'; 3 | font-style: normal; 4 | font-weight: 400; /* based on https://github.com/jonathantneal/system-font-css/blob/gh-pages/system-font.css */ 5 | src: url('IndieFlower.woff2') format('woff2'), local('.SFNSText-Regular'), 6 | local('.HelveticaNeueDeskInterface-Regular'), local('.LucidaGrandeUI'), 7 | local('Segoe UI'), local('Ubuntu'), local('Roboto-Regular'), 8 | local('DroidSans'), local('Tahoma'); 9 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, 10 | U+2000-206F, U+2074, U+20AC, U+2212, U+2215; 11 | } 12 | 13 | #imported-from-inline { 14 | color: #007000; 15 | background-color: #a0a0ff; 16 | background-image: url('icon.svg'); 17 | font-family: 'umgsfdxynf', cursive; 18 | } 19 | -------------------------------------------------------------------------------- /test/test-pages/iframes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test page 6 | 7 | 8 | 9 | 10 | main page 11 | 14 | 15 | 20 | 25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/test-pages/background-repeat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test bg-repeat 6 | 7 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/common/get_acceptable_range.ts: -------------------------------------------------------------------------------- 1 | // red, yellow, green, cyan, blue, magenta 2 | const max_bg_L = [50, 25, 30, 25, 60, 45]; 3 | const min_fg_L = [80, 40, 45, 40, 85, 75]; 4 | 5 | export function get_acceptable_range(H: number): [number, number] { 6 | // eslint-disable-next-line no-param-reassign 7 | H %= 360; // cycle it 8 | const n = Math.floor(H / 60); 9 | const dark_start = max_bg_L[n % 6]; 10 | const dark_end = max_bg_L[(n + 1) % 6]; 11 | const light_start = min_fg_L[n % 6]; 12 | const light_end = min_fg_L[(n + 1) % 6]; 13 | 14 | const pi_multiplier = (H % 60) / 60; 15 | const start_angle = (3 * Math.PI) / 2; 16 | const angle = start_angle + Math.PI * pi_multiplier; 17 | const multiplier = (Math.sin(angle) + 1) / 2; 18 | return [ 19 | Math.round(dark_start + multiplier * (dark_end - dark_start)), 20 | Math.round(light_start + multiplier * (light_end - light_start)), 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /test/test-pages/css-var/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS var() 6 | 23 | 24 | 25 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /src/common/ui-style.ts: -------------------------------------------------------------------------------- 1 | import type { Browser } from 'webextension-polyfill'; 2 | 3 | declare const browser: Browser; 4 | 5 | export async function query_style() { 6 | const css_promise = await browser.runtime.sendMessage({ 7 | action: 'query_base_style', 8 | }); 9 | 10 | let ext_style = document.getElementById('base-extension-style'); 11 | if (!ext_style) { 12 | const container = document.head || document.documentElement; 13 | ext_style = document.createElement('style'); 14 | ext_style.setAttribute('id', 'base-extension-style'); 15 | container.appendChild(ext_style); 16 | } 17 | ext_style.textContent = await css_promise; 18 | } 19 | if (document.readyState === 'loading') { 20 | document.addEventListener( 21 | 'readystatechange', 22 | (_event) => { 23 | query_style().catch((rejection) => console.error(rejection)); 24 | }, 25 | { once: true }, 26 | ); 27 | } else { 28 | query_style().catch((rejection) => console.error(rejection)); 29 | } 30 | -------------------------------------------------------------------------------- /src/methods/executors/invert.ts: -------------------------------------------------------------------------------- 1 | import type { AddonOptions, MethodExecutor } from '../../common/types'; 2 | 3 | export class InvertMethod implements MethodExecutor { 4 | window: Window; 5 | 6 | // @ts-ignore: TS6133 7 | constructor(window: Window, options: AddonOptions) { 8 | this.window = window; 9 | } 10 | 11 | load_into_window() { 12 | let el: HTMLElement | null = this.window.document.querySelector( 13 | '#mybpwaycfxccmnp-dblt-backdrop-filter', 14 | ); 15 | if (!el) { 16 | el = this.window.document.createElement('div'); 17 | el.setAttribute('id', 'mybpwaycfxccmnp-dblt-backdrop-filter'); 18 | el.style.display = 'none'; 19 | this.window.document.documentElement.appendChild(el); 20 | } 21 | } 22 | 23 | unload_from_window() { 24 | const el = this.window.document.querySelector( 25 | '#mybpwaycfxccmnp-dblt-backdrop-filter', 26 | ); 27 | if (el !== null) { 28 | el.parentElement!.removeChild(el); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/test-pages/css-flexbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | flexbox 6 | 33 | 34 | 35 |
36 | 37 |

Dark background & light text settings

38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /test/test-pages/service-worker/8080/sw.js: -------------------------------------------------------------------------------- 1 | const CACHE_NAME = 'test-cache'; 2 | const urlsToCache = [ 3 | // '/', 4 | // '/styles/main.css', 5 | // '/script/main.js' 6 | ]; 7 | 8 | // eslint-disable-next-line no-restricted-globals 9 | self.addEventListener('install', (event) => { 10 | // Perform install steps 11 | event.waitUntil( 12 | (async () => { 13 | const cache = await caches.open(CACHE_NAME); 14 | console.log('Opened cache'); 15 | return cache.addAll(urlsToCache); 16 | })(), 17 | ); 18 | }); 19 | 20 | // eslint-disable-next-line no-restricted-globals 21 | self.addEventListener('fetch', (event) => { 22 | event.respondWith( 23 | (async () => { 24 | const response = await caches.match(event.request); 25 | if (response) { 26 | console.log('cached response', response); 27 | return response; 28 | } 29 | console.log('event', event); 30 | const fetched = await fetch(event.request); 31 | console.log('fetched response', fetched); 32 | return fetched; 33 | })(), 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /test/test-pages/service-worker/8080/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Service worker 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 41 | 42 |
inline 24 | 26 |
same-origin 31 | 33 |
cross-origin 38 | 40 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /test/test-pages/stylesheets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test stylesheets 6 | 10 | 14 | 15 | 16 | test-span1 17 | test-span2 18 |
19 | NO CLASS 20 |
21 |
25 | newtab-thumbnail 26 |
27 |
31 | RANDOM CLASS 32 |
33 |
37 | one-more-exclude-class 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/methods/stylesheets/stylesheet-processor.ts: -------------------------------------------------------------------------------- 1 | import type { RenderOptions } from '../../common/types'; 2 | 3 | export const name = 'stylesheet-processor'; 4 | export function render({ 5 | default_foreground_color, 6 | default_background_color, 7 | default_link_color, 8 | default_visited_color, 9 | default_active_color, 10 | is_toplevel, 11 | }: RenderOptions) { 12 | return ` 13 | html { 14 | ${ 15 | '' /* some webpages set html's bgcolor to transparent which is becomes white so it should be !important */ 16 | }\ 17 | ${ 18 | is_toplevel 19 | ? `\ 20 | background-color: ${default_background_color} !important; 21 | ` 22 | : '' 23 | }\ 24 | ${'' /* #29 */}\ 25 | color: ${default_foreground_color} !important; 26 | } 27 | 28 | ${'' /* Legacy Attributes */}\ 29 | [bgcolor] { 30 | background-color: ${default_background_color} !important; 31 | } 32 | [text], 33 | [color] { 34 | color: ${default_foreground_color} !important; 35 | } 36 | 37 | [alink]:link:active { 38 | color: ${default_active_color} !important; 39 | } 40 | [vlink]:visited { 41 | color: ${default_visited_color} !important; 42 | } 43 | [link]:link { 44 | color: ${default_link_color} !important; 45 | } 46 | ${'' /* Legacy Attributes */}\ 47 | 48 | ${'' /* Bittorrent sync webui fix */}\ 49 | .qrCode > canvas { 50 | border: 10px white solid; 51 | } 52 | `; 53 | } 54 | -------------------------------------------------------------------------------- /src/methods/methods-with-stylesheets.ts: -------------------------------------------------------------------------------- 1 | import { methods as methods_bare } from './methods'; 2 | import type { MethodsMetadataWithStylesheets } from '../common/types'; 3 | import * as base from './stylesheets/base'; 4 | import * as invert from './stylesheets/invert'; 5 | import * as simple_css from './stylesheets/simple-css'; 6 | import * as stylesheet_processor from './stylesheets/stylesheet-processor'; 7 | 8 | // TODO: less hardcode, use names from modules 9 | for (const key of Object.keys(methods_bare)) { 10 | for (let i = 0; i < methods_bare[key].stylesheets.length; i++) { 11 | switch (methods_bare[key].stylesheets[i].name) { 12 | case 'base': 13 | methods_bare[key].stylesheets[i] = base; 14 | break; 15 | case 'invert': 16 | methods_bare[key].stylesheets[i] = invert; 17 | break; 18 | case 'simple-css': 19 | methods_bare[key].stylesheets[i] = simple_css; 20 | break; 21 | case 'stylesheet-processor': 22 | methods_bare[key].stylesheets[i] = stylesheet_processor; 23 | break; 24 | /* istanbul ignore next */ 25 | default: 26 | throw Error( 27 | `Unknown stylesheet name: ${methods_bare[key].stylesheets[i].name}`, 28 | ); 29 | } 30 | } 31 | } 32 | const methods = methods_bare as MethodsMetadataWithStylesheets; 33 | 34 | export { methods }; 35 | -------------------------------------------------------------------------------- /src/methods/stylesheets/simple-css.ts: -------------------------------------------------------------------------------- 1 | import type { RenderOptions } from '../../common/types'; 2 | 3 | export const name = 'simple-css'; 4 | export function render({ 5 | default_foreground_color, 6 | default_background_color, 7 | }: RenderOptions) { 8 | return ` 9 | * { 10 | color: ${default_foreground_color} !important; 11 | } 12 | 13 | *:not(.colorpickertile):not(.colorpicker-button-colorbox) { 14 | background-color: ${default_background_color} !important; 15 | } 16 | 17 | a *, 18 | button *, 19 | input *, 20 | optgroup *, 21 | select *, 22 | textarea * { 23 | background-color: transparent !important; 24 | text-shadow: 25 | ${default_background_color} 0 0 1pt, 26 | ${default_background_color} 0 0 2pt, 27 | ${default_background_color} 0 0 3pt, 28 | ${default_background_color} 0 0 4pt, 29 | ${default_background_color} 0 0 5pt, 30 | ${default_background_color} 0 0 5pt, 31 | ${default_background_color} 0 0 5pt !important; 32 | } 33 | a, 34 | button, 35 | input, 36 | optgroup, 37 | select, 38 | textarea { 39 | text-shadow: 40 | ${default_background_color} 0 0 1pt, 41 | ${default_background_color} 0 0 2pt, 42 | ${default_background_color} 0 0 3pt, 43 | ${default_background_color} 0 0 4pt, 44 | ${default_background_color} 0 0 5pt, 45 | ${default_background_color} 0 0 5pt, 46 | ${default_background_color} 0 0 5pt !important; 47 | } 48 | `; 49 | } 50 | -------------------------------------------------------------------------------- /src/methods/methods.ts: -------------------------------------------------------------------------------- 1 | import type { MethodsMetadataBare } from '../common/types'; 2 | 3 | export const DEFAULT_ID = '-1'; 4 | export const DISABLED_ID = '0'; 5 | export const STYLESHEET_PROCESSOR_ID = '1'; 6 | export const SIMPLE_CSS_ID = '2'; 7 | export const INVERT_ID = '3'; 8 | 9 | export const methods: MethodsMetadataBare = { 10 | [DEFAULT_ID]: { 11 | number: DEFAULT_ID, 12 | label: 'Default', 13 | stylesheets: [], 14 | affects_iframes: false, 15 | }, 16 | [DISABLED_ID]: { 17 | number: DISABLED_ID, 18 | label: 'Disabled', 19 | stylesheets: [], 20 | affects_iframes: true, 21 | }, 22 | [STYLESHEET_PROCESSOR_ID]: { 23 | number: STYLESHEET_PROCESSOR_ID, 24 | label: 'Stylesheet processor', 25 | stylesheets: [ 26 | { name: 'base' }, 27 | /* simple-css will be removed as soon as StylesheetColorProcessor do its work — 28 | this prevents bright flickering */ 29 | { name: 'simple-css' }, 30 | { name: 'stylesheet-processor' }, 31 | ], 32 | affects_iframes: false, 33 | }, 34 | [SIMPLE_CSS_ID]: { 35 | number: SIMPLE_CSS_ID, 36 | label: 'Simple CSS', 37 | stylesheets: [{ name: 'base' }, { name: 'simple-css' }], 38 | affects_iframes: false, 39 | }, 40 | [INVERT_ID]: { 41 | number: INVERT_ID, 42 | label: 'Invert', 43 | stylesheets: [{ name: 'invert' }], 44 | affects_iframes: true, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/methods/stylesheets/invert.ts: -------------------------------------------------------------------------------- 1 | import type { RenderOptions } from '../../common/types'; 2 | 3 | export const name = 'invert'; 4 | // eslint-disable-next-line no-empty-pattern 5 | export function render({}: RenderOptions) { 6 | return ` 7 | @supports (backdrop-filter: invert(1)) { 8 | #mybpwaycfxccmnp-dblt-backdrop-filter { 9 | display: block !important; 10 | position: fixed !important; 11 | top: 0 !important; 12 | bottom: 0 !important; 13 | left: 0 !important; 14 | right: 0 !important; 15 | margin: 0 !important; 16 | pointer-events: none !important; 17 | z-index: 2147483647 !important; 18 | backdrop-filter: invert(1) hue-rotate(180deg) !important; 19 | } 20 | img:not(.mwe-math-fallback-image-inline):not([alt="inline_formula"]), 21 | video { 22 | filter: invert(1) hue-rotate(180deg) !important; 23 | } 24 | } 25 | 26 | @supports not (backdrop-filter: invert(1)) { 27 | html, 28 | ${'' /* TODO: "black on transparent" mark */}\ 29 | img:not(.mwe-math-fallback-image-inline):not([alt="inline_formula"]), 30 | video, 31 | div#viewer.pdfViewer div.page { 32 | filter: invert(1) hue-rotate(180deg) !important; 33 | } 34 | ${'' /* #28 */}\ 35 | :fullscreen video, 36 | video:fullscreen { 37 | filter: none !important; 38 | } 39 | 40 | html { 41 | background-color: black !important; 42 | } 43 | } 44 | 45 | button, 46 | input, 47 | optgroup, 48 | select, 49 | textarea { 50 | background-color: white; 51 | color: black; 52 | } 53 | `; 54 | } 55 | -------------------------------------------------------------------------------- /check-coverage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 'use strict'; 3 | /* eslint-disable no-console */ 4 | 5 | const fs = require('fs'); 6 | const { execFileSync } = require('child_process'); 7 | 8 | const coverage_file = process.argv[2]; 9 | const new_coverage = JSON.parse( 10 | fs.readFileSync(coverage_file, { encoding: 'utf8' }), 11 | ); 12 | 13 | let prev_coverage_ref; 14 | if (Object.prototype.hasOwnProperty.call(process.env, 'PRE_COMMIT_FROM_REF')) { 15 | prev_coverage_ref = process.env.PRE_COMMIT_FROM_REF; 16 | } else { 17 | prev_coverage_ref = 'HEAD'; 18 | } 19 | let prev_coverage_text; 20 | try { 21 | prev_coverage_text = execFileSync( 22 | 'git', 23 | ['show', `${prev_coverage_ref}:${coverage_file}`], 24 | { encoding: 'utf8' }, 25 | ); 26 | } catch (e) { 27 | console.log('Previous coverage is not found'); 28 | prev_coverage_text = '{}'; 29 | } 30 | const prev_coverage = JSON.parse(prev_coverage_text); 31 | 32 | let decreased = false; 33 | Object.keys(new_coverage).forEach((f) => { 34 | if (f === 'total') { 35 | return; 36 | } 37 | Object.keys(new_coverage[f]) 38 | .sort() 39 | .forEach((what) => { 40 | let prev_val = 0; 41 | if (f in prev_coverage) { 42 | prev_val = parseFloat(prev_coverage[f][what].pct); 43 | } 44 | const new_val = parseFloat(new_coverage[f][what].pct); 45 | if (new_val < prev_val) { 46 | decreased = true; 47 | console.log(`${f} ${what}: ${prev_val} -> ${new_val}`); 48 | } 49 | }); 50 | }); 51 | if (decreased) { 52 | process.exit(2); 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "rollup -c --failAfterWarnings", 4 | "test": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' nyc --reporter='../../coverage-relative-json-summary' --reporter=html --reporter=text mocha" 5 | }, 6 | "devDependencies": { 7 | "@rollup/plugin-commonjs": "^24.0.0", 8 | "@rollup/plugin-node-resolve": "^15.0.1", 9 | "@rollup/plugin-terser": "^0.2.1", 10 | "@rollup/plugin-typescript": "^10.0.1", 11 | "@types/mocha": "^10.0.1", 12 | "@types/stylelint": "^13.13.3", 13 | "@types/webextension-polyfill": "^0.9.2", 14 | "@typescript-eslint/eslint-plugin": "^5.47.1", 15 | "@typescript-eslint/parser": "^5.47.1", 16 | "assert": "^2.0.0", 17 | "eslint": "^8.31.0", 18 | "eslint-config-prettier": "^8.6.0", 19 | "eslint-import-resolver-typescript": "^3.5.2", 20 | "eslint-plugin-import": "^2.26.0", 21 | "eslint-plugin-svelte": "^2.14.1", 22 | "mocha": "^10.2.0", 23 | "npm-check-updates": "^16.6.2", 24 | "nyc": "^15.1.0", 25 | "prettier": "npm:@btmills/prettier@^2.7.1", 26 | "prettier-plugin-svelte": "^2.9.0", 27 | "rollup": "^3.9.0", 28 | "rollup-plugin-clear": "^2.0.7", 29 | "rollup-plugin-copy": "^3.4.0", 30 | "rollup-plugin-css-only": "^4.3.0", 31 | "rollup-plugin-svelte": "^7.1.0", 32 | "stylelint": "^14.16.1", 33 | "stylelint-config-standard": "^29.0.0", 34 | "svelte": "^3.55.0", 35 | "svelte-check": "^3.0.1", 36 | "svelte-preprocess": "^5.0.0", 37 | "ts-loader": "^9.4.2", 38 | "ts-node": "^10.9.1", 39 | "typescript": "^4.9.4" 40 | }, 41 | "dependencies": { 42 | "await-lock": "^2.2.2", 43 | "csscolorparser-ts": "^1.1.1", 44 | "tldts": "^5.7.103" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Dark Background and Light Text", 4 | "version": "0.7.6", 5 | "description": "Makes every page to have light text on dark background (exact colors are customizable)", 6 | "icons": { 7 | "48": "icons/icon48.png" 8 | }, 9 | "applications": { 10 | "gecko": { 11 | "id": "jid1-QoFqdK4qzUfGWQ@jetpack" 12 | } 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": [""], 17 | "js": ["content.js"], 18 | "run_at": "document_start", 19 | "match_about_blank": true, 20 | "all_frames": true 21 | } 22 | ], 23 | "background": { 24 | "scripts": ["background.js"] 25 | }, 26 | "commands": { 27 | "global_toggle_hotkey": { 28 | "suggested_key": { 29 | "default": "F2" 30 | }, 31 | "description": "Toggle enabled globally" 32 | }, 33 | "tab_toggle_hotkey": { 34 | "suggested_key": { 35 | "default": "Ctrl+Alt+D" 36 | }, 37 | "description": "Toggle enabled for current tab" 38 | } 39 | }, 40 | "browser_action": { 41 | "browser_style": false, 42 | "default_icon": { 43 | "16": "icons/icon16.png", 44 | "24": "icons/icon24.png", 45 | "32": "icons/icon32.png", 46 | "48": "icons/icon48.png", 47 | "64": "icons/icon64.png" 48 | }, 49 | "default_title": "Dark Background and Light Text", 50 | "default_popup": "ui/configure-for-current-tab-panel.html" 51 | }, 52 | "options_ui": { 53 | "page": "ui/preferences.html", 54 | "browser_style": false, 55 | "open_in_tab": true 56 | }, 57 | "permissions": [ 58 | "", 59 | "tabs", 60 | "storage", 61 | "browserSettings", 62 | "webRequest", 63 | "webRequestBlocking" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /test/test-pages/input-color/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | color input 6 | 7 | 8 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
ololo1
ololo2
ololo3
ololo4
ololo5
67 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.1 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-json 9 | - id: check-added-large-files 10 | - repo: https://github.com/jumanjihouse/pre-commit-hooks.git 11 | rev: 2.1.5 12 | hooks: 13 | - id: shellcheck 14 | args: ['--shell=bash', '--color=always', '--external-sources'] 15 | additional_dependencies: [] 16 | - repo: local 17 | hooks: 18 | - id: prettier 19 | name: prettier 20 | entry: npx prettier --write --list-different --ignore-unknown 21 | language: system 22 | pass_filenames: true 23 | types: [text] 24 | args: [] 25 | require_serial: false 26 | - id: eslint 27 | name: eslint 28 | entry: npx eslint 29 | language: system 30 | pass_filenames: true 31 | types_or: [ts, javascript] 32 | args: [] 33 | require_serial: false 34 | - repo: local 35 | hooks: 36 | - id: tests 37 | name: Mocha tests 38 | language: system 39 | entry: npm test 40 | always_run: true 41 | pass_filenames: false 42 | require_serial: true 43 | - repo: local 44 | hooks: 45 | - id: coverage 46 | name: Ensure that coverage hasn't decreased per-file 47 | language: system 48 | entry: ./check-coverage last-coverage-summary.json 49 | always_run: true 50 | pass_filenames: false 51 | require_serial: true 52 | - repo: local 53 | hooks: 54 | - id: svelte-check 55 | name: Svelte check 56 | language: system 57 | entry: npx svelte-check --fail-on-warnings 58 | always_run: true 59 | pass_filenames: false 60 | require_serial: true 61 | - repo: local 62 | hooks: 63 | - id: rollup 64 | name: Rollup dry run 65 | language: system 66 | entry: sh -c 'ADDON_DIST_DIR="$(mktemp -d)"; export ADDON_DIST_DIR; npm run build; EXITCODE=$?; rm -r "${ADDON_DIST_DIR}"; exit "${EXITCODE}"' 67 | always_run: true 68 | pass_filenames: false 69 | require_serial: true 70 | -------------------------------------------------------------------------------- /test/test-utils.js: -------------------------------------------------------------------------------- 1 | /* var utils = require('./utils.js'); 2 | 3 | exports['test count_char_in_string'] = function(assert) { 4 | return assert.ok( 5 | (utils.count_char_in_string('(', '123(asdf(asdf))ads()asdf') === 3) && 6 | (utils.count_char_in_string('1', '11111111111111111111111') === 23) && 7 | (utils.count_char_in_string(' ', '111111 111 111 1111111 ') === 4), 8 | 'count_subs_in_string'); 9 | }; 10 | 11 | exports['test split_background_image'] = function(assert) { 12 | let res = utils.split_background_image( 13 | 'url("../../img/icons/go-arrow.png?ad8fa66"), linear-gradient(#84C63C, #489615)' 14 | ); 15 | return assert.ok(( 16 | res[0] == 'url("../../img/icons/go-arrow.png?ad8fa66")' && 17 | res[1] == 'linear-gradient(#84C63C,#489615)' && 18 | res.length == 2 19 | ), 'split_background_image'); 20 | }; 21 | /* 22 | exports['test RGB_TO_HSL speed'] = function(assert) { 23 | let color_utils = require('./color_utils.js'); 24 | let start = (new Date()).getTime(); 25 | for (let i = 0; i < 1000000; i++) 26 | color_utils.RGB_to_HSL([128, 128, 200]); 27 | let end = (new Date()).getTime(); 28 | let time = end - start; 29 | console.log('Execution time: ' + time); 30 | return assert.ok(true, 'time measure'); 31 | }; */ 32 | /* 33 | exports['test count_char_in_string speed'] = function(assert) { 34 | let start = (new Date()).getTime(); 35 | for (let i = 0; i < 2000000; i++) { 36 | utils.count_char_in_string('1', '11111111111111111111111'); 37 | } 38 | let end = (new Date()).getTime(); 39 | let time = end - start; 40 | console.log('Execution time: ' + time); 41 | return assert.ok(true, 'just speed') 42 | }; 43 | exports['test brackets_aware_split speed'] = function(assert) { 44 | let start = (new Date()).getTime(); 45 | for (let i = 0; i < 1000000; i++) { 46 | utils.brackets_aware_split( 47 | 'url("../../img/icons/go-arrow.png?ad8fa66"), linear-gradient(#84C63C, #489615)' 48 | ) 49 | } 50 | let end = (new Date()).getTime(); 51 | let time = end - start; 52 | console.log('Execution time: ' + time); 53 | return assert.ok(true, 'just speed') 54 | }; 55 | 56 | require("sdk/test").run(exports); 57 | */ 58 | -------------------------------------------------------------------------------- /test/test-pages/cors-stylesheets/8080/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CORS stylesheets 6 | 24 | 33 | 37 | 41 | 42 | 43 | 44 |
pure-inline-style (icon: headphones jack)
45 |
46 | inline-with-import (icon: headphones jack) 47 |
48 |
49 | imported-from-inline (icon: graphic tablet, font: Indie Flower, media: 50 | min-width: 600px) 51 |
52 |
53 | linked-same-origin (icon: headphones jack) 54 |
55 |
56 | cross-origin-import-from-same-origin-linked (icon: graphic tablet) 57 |
58 |
59 | cross-origin-stylesheet (icon: wired network) 60 |
61 |
62 | cross-origin-stylesheet-with-cross-origin-import (icon: graphic tablet) 63 |
64 |
65 | imported-from-cross-origin (icon: sdcard, media: min-width: 600px) 66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /test/test-pages/canvas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | canvas 6 | 7 | 8 | 23 | 24 | 25 |
26 | debug_label
27 | color_itself
28 | just regular text lalala just regular text lalala 29 | Background example 30 | just regular text lalala just regular text lalala 31 | Foreground example 32 | just regular text lalala just regular text lalala 33 |
34 | 35 | 36 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import node_resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import copy from 'rollup-plugin-copy'; 5 | import clear from 'rollup-plugin-clear'; 6 | import terser from '@rollup/plugin-terser'; 7 | 8 | import svelte from 'rollup-plugin-svelte'; 9 | import css from 'rollup-plugin-css-only'; 10 | import autoPreprocess from 'svelte-preprocess'; 11 | 12 | export default (args) => { 13 | let output_opts = {}; 14 | const common_plugins = [ 15 | typescript({ 16 | sourceMap: args.watch === true, 17 | }), 18 | node_resolve(), 19 | commonjs(), 20 | ]; 21 | const dest_dir = process.env.ADDON_DIST_DIR ?? 'dist'; 22 | if (args.watch === true) { 23 | output_opts = { 24 | plugins: [], 25 | format: 'iife', 26 | sourcemap: true, 27 | }; 28 | } else { 29 | output_opts = { 30 | plugins: [terser()], 31 | format: 'iife', 32 | sourcemap: false, 33 | }; 34 | } 35 | return [ 36 | { 37 | input: 'src/content/index.ts', 38 | plugins: [ 39 | clear({ 40 | targets: [dest_dir], 41 | }), 42 | copy({ 43 | targets: [ 44 | { src: 'manifest.json', dest: dest_dir }, 45 | { src: 'icons/*', dest: `${dest_dir}/icons/` }, 46 | { src: 'ui/*', dest: `${dest_dir}/ui/` }, 47 | ], 48 | }), 49 | ...common_plugins, 50 | ], 51 | output: { 52 | file: `${dest_dir}/content.js`, 53 | ...output_opts, 54 | }, 55 | }, 56 | { 57 | input: 'src/background/index.ts', 58 | plugins: [...common_plugins], 59 | output: { 60 | file: `${dest_dir}/background.js`, 61 | ...output_opts, 62 | }, 63 | }, 64 | { 65 | input: 'src/preferences/main.ts', 66 | plugins: [ 67 | svelte({ 68 | preprocess: autoPreprocess({ sourceMap: true }), 69 | compilerOptions: { 70 | // enable run-time checks when not in production 71 | dev: args.watch === true, 72 | }, 73 | }), 74 | css({ output: 'preferences.css' }), 75 | ...common_plugins, 76 | ], 77 | output: { 78 | file: `${dest_dir}/preferences.js`, 79 | name: 'app', 80 | ...output_opts, 81 | }, 82 | }, 83 | { 84 | input: 'src/browser-action/index.ts', 85 | plugins: [...common_plugins], 86 | output: { 87 | file: `${dest_dir}/browser-action.js`, 88 | ...output_opts, 89 | }, 90 | }, 91 | ]; 92 | }; 93 | -------------------------------------------------------------------------------- /test/test-pages/simple-node-server.js: -------------------------------------------------------------------------------- 1 | // based on 2 | // https://developer.mozilla.org/en-US/docs/Learn/Server-side/Node_server_without_framework 3 | 4 | const { createServer } = require('http'); 5 | const fs = require('fs').promises; 6 | const get_extname = require('path').extname; 7 | 8 | console.log( 9 | 'This server must be used only for testing with trusted code. Do not use it on production or anywhere except for localhost!', 10 | ); 11 | 12 | exports.serve = ({ 13 | port = 8080, 14 | mutateHeaders, 15 | mutateFilePath, 16 | index = 'index.html', 17 | }) => { 18 | const IPs = ['127.0.0.1', '::1']; 19 | 20 | const mimeTypes = { 21 | '.html': 'text/html', 22 | '.js': 'text/javascript', 23 | '.css': 'text/css', 24 | '.json': 'application/json', 25 | '.png': 'image/png', 26 | '.jpg': 'image/jpg', 27 | '.gif': 'image/gif', 28 | '.svg': 'image/svg+xml', 29 | '.wav': 'audio/wav', 30 | '.mp4': 'video/mp4', 31 | '.woff': 'application/font-woff', 32 | '.ttf': 'application/font-ttf', 33 | '.eot': 'application/vnd.ms-fontobject', 34 | '.otf': 'application/font-otf', 35 | '.wasm': 'application/wasm', 36 | }; 37 | 38 | IPs.forEach((bind_to) => { 39 | createServer(async (request, response) => { 40 | let filePath = request.url; 41 | if (index && filePath.endsWith('/')) { 42 | filePath += index; 43 | } 44 | if (mutateFilePath) { 45 | filePath = mutateFilePath({ 46 | filePath, 47 | request, 48 | bind_address: bind_to, 49 | }); 50 | } 51 | console.log(`${request.method} ${filePath}`); 52 | 53 | const extname = String(get_extname(filePath)).toLowerCase(); 54 | 55 | const contentType = mimeTypes[extname] || 'application/octet-stream'; 56 | 57 | let body; 58 | let code; 59 | try { 60 | // no security precautions here, use only for testing with trusted code! 61 | body = await fs.readFile(filePath); 62 | code = 200; 63 | } catch (e) { 64 | console.error(e); 65 | body = '404 Not Found'; 66 | code = 404; 67 | } 68 | 69 | let headers = { 70 | 'Content-Type': contentType, 71 | }; 72 | if (mutateHeaders) { 73 | headers = mutateHeaders({ 74 | headers, 75 | request, 76 | }); 77 | } 78 | 79 | response.writeHead(code, headers); 80 | response.end(body, 'utf-8'); 81 | }).listen(port, bind_to); 82 | console.log( 83 | `Server running at http://${ 84 | bind_to.indexOf(':') >= 0 ? `[${bind_to}]` : bind_to 85 | }:${port}/`, 86 | ); 87 | }); 88 | }; 89 | -------------------------------------------------------------------------------- /ui/internal.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark-background-light-text-add-on-foreground-color: black; 3 | --dark-background-light-text-add-on-background-color: white; 4 | --dark-background-light-text-add-on-link-color: #0000ff; /*TODO: better default colors */ 5 | --dark-background-light-text-add-on-visited-color: #a000a0; 6 | --dark-background-light-text-add-on-active-color: #ff0000; 7 | --dark-background-light-text-add-on-selection-color: #8080ff; 8 | } 9 | 10 | html, 11 | body { 12 | font: caption; 13 | color: var(--dark-background-light-text-add-on-foreground-color); 14 | background-color: var(--dark-background-light-text-add-on-background-color); 15 | } 16 | 17 | input, 18 | button, 19 | select { 20 | box-sizing: border-box; 21 | } 22 | 23 | button, 24 | select, 25 | input[type='color'] { 26 | cursor: pointer; 27 | } 28 | 29 | button::-moz-focus-inner { 30 | border: 0; 31 | } 32 | 33 | input[type='checkbox'], 34 | input[type='radio'] { 35 | height: 1em; 36 | width: 1em; 37 | font-size: 1em; 38 | } 39 | 40 | select, 41 | input[type='color'], 42 | input[type='text'], 43 | button { 44 | -moz-appearance: none; 45 | color: var(--dark-background-light-text-add-on-foreground-color); 46 | font-size: 1em; 47 | line-height: 1.2em; 48 | padding: 0.5em 0.5em; 49 | border-radius: 0.2em; 50 | border-width: 1px; 51 | /* similar selector from base.css has higher specificity and should overwrite color below but it doesn't (bug?) */ 52 | border-color: var(--dark-background-light-text-add-on-foreground-color); 53 | border-style: solid; 54 | background-color: var(--dark-background-light-text-add-on-background-color); 55 | transition-duration: 0.3s; 56 | transition-property: border-color, box-shadow; 57 | } 58 | 59 | input[type='text'] { 60 | box-shadow: inset 0 0 0.15em 0.15em transparent; 61 | } 62 | 63 | input[type='text']:focus { 64 | box-shadow: inset 0 0 0.15em 0.15em 65 | var(--dark-background-light-text-add-on-selection-color) !important; 66 | border-color: var( 67 | --dark-background-light-text-add-on-selection-color 68 | ) !important; 69 | } 70 | 71 | select, 72 | input[type='color'], 73 | button { 74 | box-shadow: 0 0 0.15em 0.15em transparent !important; 75 | } 76 | 77 | select:focus, 78 | input[type='color']:focus, 79 | button:focus { 80 | box-shadow: 0 0 0.15em 0.15em 81 | var(--dark-background-light-text-add-on-selection-color) !important; 82 | border-color: var( 83 | --dark-background-light-text-add-on-selection-color 84 | ) !important; 85 | } 86 | 87 | select { 88 | text-overflow: ellipsis; 89 | background-image: url('data:image/svg+xml;utf8,'); 90 | background-position: right center; 91 | background-repeat: no-repeat; 92 | padding-right: 1em; 93 | background-size: 1em; 94 | } 95 | -------------------------------------------------------------------------------- /test/test-pages/inline-style/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inline style test 6 | 18 | 19 | 20 | 21 | 22 |
23 | 37 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /test/test-pages/invert-test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Invert test 6 | 39 | 40 | 41 | 42 |
43 |
test black text
44 |
test red text
45 | 46 | 47 | 50 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | 69 | 70 | 77 | 78 | 79 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /ui/configure-for-current-tab-panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dark bg & Light text settings 6 | 7 | 8 | 9 | 10 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |

Loading...

128 | 129 | 130 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | webextensions: true, 7 | }, 8 | extends: ['eslint:recommended', 'prettier'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaVersion: 12, 12 | sourceType: 'module', 13 | }, 14 | plugins: ['@typescript-eslint'], 15 | settings: { 16 | 'import/resolver': { 17 | typescript: {}, 18 | }, 19 | }, 20 | rules: { 21 | // most overrides are based on airbnb rule with some change 22 | camelcase: ['off'], 23 | 'import/prefer-default-export': ['off'], 24 | 'no-console': [ 25 | 'error', 26 | { 27 | allow: ['warn', 'error'], 28 | }, 29 | ], 30 | 'no-continue': ['off'], 31 | 'no-else-return': ['off'], 32 | 'no-lonely-if': ['off'], 33 | 'no-mixed-operators': [ 34 | 'error', 35 | { 36 | allowSamePrecedence: true, 37 | groups: [ 38 | ['%', '**'], 39 | ['%', '+'], 40 | ['%', '-'], 41 | ['%', '*'], 42 | ['%', '/'], 43 | ['/', '*'], 44 | ['&', '|', '<<', '>>', '>>>'], 45 | ['==', '!=', '===', '!=='], 46 | ['&&', '||'], 47 | ], 48 | }, 49 | ], 50 | 'no-multi-spaces': [ 51 | 'error', 52 | { 53 | ignoreEOLComments: true, 54 | }, 55 | ], 56 | 'no-multiple-empty-lines': [ 57 | 'error', 58 | { 59 | max: 2, 60 | maxBOF: 0, 61 | maxEOF: 0, 62 | }, 63 | ], 64 | 'no-plusplus': ['off'], 65 | 'no-restricted-syntax': [ 66 | 'error', 67 | { 68 | message: 69 | 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', 70 | selector: 'ForInStatement', 71 | }, 72 | { 73 | message: 74 | 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', 75 | selector: 'LabeledStatement', 76 | }, 77 | { 78 | message: 79 | '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', 80 | selector: 'WithStatement', 81 | }, 82 | ], 83 | 'no-use-before-define': [ 84 | 'error', 85 | { 86 | classes: true, 87 | functions: false, 88 | variables: true, 89 | }, 90 | ], 91 | 'nonblock-statement-body-position': ['off'], 92 | 'prefer-destructuring': [ 93 | 'off', 94 | { 95 | array: false, 96 | object: true, 97 | }, 98 | ], 99 | 'require-await': 'error', 100 | // ESLint does not detects modules properly 101 | // in particular, it treats check-coverage as a module 102 | strict: 'off', 103 | 'lines-around-directive': 'off', 104 | }, 105 | overrides: [ 106 | { 107 | files: ['*.ts'], 108 | rules: { 109 | 'consistent-return': ['off'], 110 | 'no-redeclare': ['off'], 111 | 'no-undef': ['off'], 112 | 'no-unused-vars': ['off'], 113 | 'no-useless-return': ['off'], 114 | 'no-dupe-class-members': 'off', 115 | '@typescript-eslint/no-dupe-class-members': ['error'], 116 | 117 | 'lines-between-class-members': 'off', 118 | }, 119 | }, 120 | { 121 | files: ['./test/**'], 122 | rules: { 123 | 'no-console': ['off'], 124 | }, 125 | }, 126 | ], 127 | }; 128 | -------------------------------------------------------------------------------- /src/background/lib.ts: -------------------------------------------------------------------------------- 1 | import type { WebRequest } from 'webextension-polyfill'; 2 | 3 | export function modify_csp( 4 | header: WebRequest.HttpHeadersItemType, 5 | ): WebRequest.HttpHeadersItemType { 6 | if (header.name.toLowerCase() === 'content-security-policy') { 7 | const new_values = header.value!.split(',').map((value) => { 8 | const directives: { [key: string]: string[] } = {}; 9 | for (const directive of value 10 | .split(';') 11 | .map((d) => d.trim()) 12 | .filter((d) => d.length > 0)) { 13 | const parts = directive 14 | .split(' ') 15 | .map((p) => p.trim()) 16 | .filter((p) => p.length > 0); 17 | const name = parts.shift()!; 18 | directives[name] = parts; 19 | } 20 | 21 | if (Object.prototype.hasOwnProperty.call(directives, 'style-src')) { 22 | if (directives['style-src'].includes("'unsafe-inline'")) { 23 | return value; 24 | } else if ( 25 | directives['style-src'].length === 1 26 | && directives['style-src'][0] === "'none'" 27 | ) { 28 | directives['style-src'] = ["'unsafe-inline'"]; 29 | } else { 30 | directives['style-src'].push("'unsafe-inline'"); 31 | } 32 | } else if ( 33 | Object.prototype.hasOwnProperty.call(directives, 'default-src') 34 | ) { 35 | if (directives['default-src'].includes("'unsafe-inline'")) { 36 | return value; 37 | } else if ( 38 | directives['default-src'].length === 1 39 | && directives['default-src'][0] === "'none'" 40 | ) { 41 | directives['style-src'] = ["'unsafe-inline'"]; 42 | } else { 43 | directives['style-src'] = directives['default-src'].slice(); 44 | directives['style-src'].push("'unsafe-inline'"); 45 | } 46 | } else { 47 | return value; 48 | } 49 | 50 | return Object.keys(directives) 51 | .map((k) => `${k} ${directives[k].join(' ')}`) 52 | .join('; '); 53 | }); 54 | return { 55 | name: header.name, 56 | value: new_values.join(' , '), 57 | }; 58 | } else { 59 | return header; 60 | } 61 | } 62 | 63 | export function modify_cors( 64 | headers: WebRequest.HttpHeaders, 65 | details: WebRequest.OnHeadersReceivedDetailsType, 66 | ) { 67 | // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1393022 68 | if (details.documentUrl) { 69 | const url_obj = new URL(details.documentUrl); 70 | let done = false; 71 | for (const header of headers) { 72 | if (header.name.toLowerCase() === 'access-control-allow-origin') { 73 | header.value = url_obj.origin; 74 | done = true; 75 | } 76 | } 77 | if (!done) { 78 | headers.push({ 79 | name: 'Access-Control-Allow-Origin', 80 | value: url_obj.origin, 81 | }); 82 | } 83 | } 84 | return headers; 85 | } 86 | 87 | function splitver(ver: string): number[] { 88 | return ver.split('.').map((s) => parseInt(s, 10)); 89 | } 90 | 91 | /** Very simple "less than" for version strings 92 | * Does **not** handle alpha, beta, etc postfixes, only dot-separated numbers */ 93 | export function version_lt(target: string, ref: string): boolean { 94 | const t_a = splitver(target); 95 | const r_a = splitver(ref); 96 | 97 | const length = Math.max(t_a.length, r_a.length); 98 | for (let i = 0; i < length; i++) { 99 | const t = t_a[i] ?? 0; 100 | const r = r_a[i] ?? 0; 101 | if (t === r) { 102 | continue; 103 | } 104 | return t < r; 105 | } 106 | return false; 107 | } 108 | -------------------------------------------------------------------------------- /ui/preferences.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dark bg & Light text preferences 6 | 7 | 8 | 9 | 10 | 11 | 12 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /test/test-render-valid-css.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { describe } from 'mocha'; 3 | import { lint, LintResult } from 'stylelint'; 4 | import type { RenderOptions } from '../src/common/types'; 5 | import { methods } from '../src/methods/methods-with-stylesheets'; 6 | 7 | function format_error(results: LintResult[], source: string): string { 8 | const result_lines: string[] = []; 9 | for (const result of results) { 10 | result_lines.push(`${result.warnings.length} errors`); 11 | for (const error of result.warnings) { 12 | result_lines.push( 13 | `\n\n${error.line}:${error.column}: ${error.severity}: ${error.text}\n`, 14 | ); 15 | source.split('\n').forEach((line, line_number, splitted) => { 16 | const pad = splitted.length.toString().length; 17 | if (line_number - error.line >= -6 && line_number - error.line < 5) { 18 | const line_number_1 = line_number + 1; 19 | result_lines.push( 20 | `${line_number_1.toString().padStart(pad, ' ')}| ${ 21 | error.line === line_number_1 ? '>' : ' ' 22 | } ${line}`, 23 | ); 24 | if (line_number_1 === error.line) { 25 | result_lines.push( 26 | `${' '.repeat(pad)}| ${' '.repeat(error.column)}^`, 27 | ); 28 | } 29 | } 30 | }); 31 | } 32 | for (const error of result.invalidOptionWarnings) { 33 | result_lines.push(error.text); 34 | } 35 | } 36 | return result_lines.join('\n'); 37 | } 38 | 39 | describe('Test if valid CSS are rendered', () => { 40 | new Set( 41 | Object.values(methods) 42 | .map((m) => m.stylesheets || []) 43 | .flat(), 44 | ).forEach((renderer) => { 45 | const options: RenderOptions = { 46 | default_foreground_color: '#123456', 47 | default_background_color: '#123456', 48 | default_link_color: '#123456', 49 | default_visited_color: '#123456', 50 | default_active_color: '#123456', 51 | default_selection_color: '#123456', 52 | is_toplevel: true, 53 | is_darkbg: true, 54 | }; 55 | for (const [is_toplevel, is_darkbg] of [ 56 | [true, true], 57 | [false, false], 58 | [true, false], 59 | [false, true], 60 | ]) { 61 | options.is_toplevel = is_toplevel; 62 | options.is_darkbg = is_darkbg; 63 | const options_copy = { ...options }; 64 | it(`${renderer.name} ${JSON.stringify({ 65 | is_toplevel, 66 | is_darkbg, 67 | })}`, async () => { 68 | const rendered = renderer.render(options_copy); 69 | const result_object = await lint({ 70 | config: { 71 | extends: 'stylelint-config-standard', 72 | rules: { 73 | 'color-hex-length': [ 74 | 'long', 75 | { 76 | string: 'long', 77 | }, 78 | ], 79 | 'no-descending-specificity': [ 80 | true, 81 | { 82 | ignore: ['selectors-within-list'], 83 | }, 84 | ], 85 | 'at-rule-disallowed-list': [ 86 | 'document', // obsolete 87 | ], 88 | 'rule-empty-line-before': null, 89 | indentation: 2, 90 | // forbid comments in rendered stylesheet 91 | 'comment-pattern': '(?!)', 92 | 'selector-not-notation': 'simple', 93 | 'no-empty-first-line': null, 94 | 'selector-class-pattern': null, 95 | 'selector-id-pattern': null, 96 | 'selector-no-vendor-prefix': null, 97 | 'property-no-vendor-prefix': null, 98 | }, 99 | }, 100 | code: rendered, 101 | formatter: 'string', 102 | }); 103 | assert( 104 | !result_object.errored, 105 | `${format_error(result_object.results, rendered)}\n\n${ 106 | '' /* result_object.output */ 107 | }`, 108 | ); 109 | }); 110 | } 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | export type RGBA = [number, number, number, number]; 2 | export type RGB = [number, number, number]; 3 | export type HSL = [number, number, number]; 4 | export interface HSV_obj { 5 | H: number; 6 | S: number; 7 | V: number; 8 | } 9 | export interface RGB_obj { 10 | R: number; 11 | G: number; 12 | B: number; 13 | } 14 | 15 | export type MethodIndex = '-1' | '0' | '1' | '2' | '3'; 16 | export interface ConfiguredPages { 17 | [key: string]: MethodIndex; 18 | } 19 | export interface ConfiguredTabs { 20 | [key: number]: MethodIndex; 21 | } 22 | export interface AddonOptions { 23 | enabled: boolean; 24 | global_toggle_hotkey: string; 25 | tab_toggle_hotkey: string; 26 | default_method: MethodIndex; 27 | default_foreground_color: string; 28 | default_background_color: string; 29 | default_link_color: string; 30 | default_visited_color: string; 31 | default_active_color: string; 32 | default_selection_color: string; 33 | do_not_set_overrideDocumentColors_to_never: boolean; 34 | configured_pages: ConfiguredPages; 35 | } 36 | export type PrefsType = string | number | boolean | ConfiguredPages; 37 | interface Preference { 38 | type: 'bool' | 'menulist' | 'color' | 'configured_pages'; 39 | name: string; 40 | value: PrefsType; 41 | options?: Array<{ label: string; value: string }>; 42 | title: string; 43 | } 44 | export interface BoolPreference extends Preference { 45 | type: 'bool'; 46 | name: string; 47 | value: boolean; 48 | options: undefined; 49 | title: string; 50 | } 51 | export interface MenuListPreference extends Preference { 52 | type: 'menulist'; 53 | name: string; 54 | value: number; 55 | options: Array<{ label: string; value: string }>; 56 | title: string; 57 | } 58 | export interface ColorPreference extends Preference { 59 | type: 'color'; 60 | name: string; 61 | value: string; 62 | options: undefined; 63 | title: string; 64 | } 65 | export interface ConfiguredPagesPreference extends Preference { 66 | type: 'configured_pages'; 67 | name: string; 68 | value: ConfiguredPages; 69 | options: undefined; 70 | title: string; 71 | } 72 | export type Preferences = ( 73 | | BoolPreference 74 | | MenuListPreference 75 | | ColorPreference 76 | | ConfiguredPagesPreference 77 | )[]; 78 | 79 | export interface RenderOptions { 80 | default_foreground_color: string; 81 | default_background_color: string; 82 | default_link_color: string; 83 | default_visited_color: string; 84 | default_active_color: string; 85 | default_selection_color: string; 86 | is_toplevel: boolean; 87 | is_darkbg: boolean; 88 | } 89 | 90 | export interface StylesheetRendererBare { 91 | name: string; 92 | } 93 | 94 | export interface StylesheetRenderer extends StylesheetRendererBare { 95 | render: (options: RenderOptions) => string; 96 | } 97 | 98 | export interface MethodMetadataBare { 99 | label: string; 100 | number: MethodIndex; 101 | affects_iframes: boolean; 102 | stylesheets: StylesheetRendererBare[]; 103 | } 104 | 105 | export interface MethodMetadataWithStylesheets extends MethodMetadataBare { 106 | stylesheets: StylesheetRenderer[]; 107 | } 108 | export interface MethodExecutor { 109 | load_into_window(): void; 110 | unload_from_window(): void; 111 | } 112 | export interface MethodExecutorStatic { 113 | new (window: Window, options: AddonOptions): MethodExecutor; 114 | } 115 | export interface MethodMetadataWithExecutors extends MethodMetadataBare { 116 | executor: MethodExecutorStatic | null; 117 | } 118 | export type MethodsMetadataBare = { 119 | [key: string /* MethodIndex */]: MethodMetadataBare; 120 | }; 121 | export type MethodsMetadataWithStylesheets = { 122 | [key: string /* MethodIndex */]: MethodMetadataWithStylesheets; 123 | }; 124 | export type MethodsMetadataWithExecutors = { 125 | [key: string /* MethodIndex */]: MethodMetadataWithExecutors; 126 | }; 127 | 128 | export interface DefaultColors { 129 | default_light_color: RGBA; 130 | default_dark_color: RGBA; 131 | } 132 | 133 | // eslint bug? 134 | // eslint-disable-next-line no-shadow 135 | export const enum CallbackID { 136 | INSERT_CSS, 137 | REMOVE_CSS, 138 | } 139 | -------------------------------------------------------------------------------- /last-coverage-summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/background/lib.ts": { 3 | "lines": { 4 | "total": 53, 5 | "covered": 53, 6 | "skipped": 0, 7 | "pct": 100 8 | }, 9 | "functions": { 10 | "total": 11, 11 | "covered": 11, 12 | "skipped": 0, 13 | "pct": 100 14 | }, 15 | "statements": { 16 | "total": 55, 17 | "covered": 55, 18 | "skipped": 0, 19 | "pct": 100 20 | }, 21 | "branches": { 22 | "total": 30, 23 | "covered": 30, 24 | "skipped": 0, 25 | "pct": 100 26 | } 27 | }, 28 | "src/common/generate-urls.ts": { 29 | "lines": { 30 | "total": 75, 31 | "covered": 75, 32 | "skipped": 0, 33 | "pct": 100 34 | }, 35 | "functions": { 36 | "total": 4, 37 | "covered": 4, 38 | "skipped": 0, 39 | "pct": 100 40 | }, 41 | "statements": { 42 | "total": 78, 43 | "covered": 78, 44 | "skipped": 0, 45 | "pct": 100 46 | }, 47 | "branches": { 48 | "total": 78, 49 | "covered": 75, 50 | "skipped": 0, 51 | "pct": 96.15 52 | } 53 | }, 54 | "src/common/smart-generate-urls.ts": { 55 | "lines": { 56 | "total": 6, 57 | "covered": 6, 58 | "skipped": 0, 59 | "pct": 100 60 | }, 61 | "functions": { 62 | "total": 2, 63 | "covered": 2, 64 | "skipped": 0, 65 | "pct": 100 66 | }, 67 | "statements": { 68 | "total": 6, 69 | "covered": 6, 70 | "skipped": 0, 71 | "pct": 100 72 | }, 73 | "branches": { 74 | "total": 3, 75 | "covered": 3, 76 | "skipped": 0, 77 | "pct": 100 78 | } 79 | }, 80 | "src/methods/methods-with-stylesheets.ts": { 81 | "lines": { 82 | "total": 18, 83 | "covered": 18, 84 | "skipped": 0, 85 | "pct": 100 86 | }, 87 | "functions": { 88 | "total": 0, 89 | "covered": 0, 90 | "skipped": 0, 91 | "pct": 100 92 | }, 93 | "statements": { 94 | "total": 19, 95 | "covered": 19, 96 | "skipped": 0, 97 | "pct": 100 98 | }, 99 | "branches": { 100 | "total": 4, 101 | "covered": 4, 102 | "skipped": 0, 103 | "pct": 100 104 | } 105 | }, 106 | "src/methods/methods.ts": { 107 | "lines": { 108 | "total": 6, 109 | "covered": 6, 110 | "skipped": 0, 111 | "pct": 100 112 | }, 113 | "functions": { 114 | "total": 0, 115 | "covered": 0, 116 | "skipped": 0, 117 | "pct": 100 118 | }, 119 | "statements": { 120 | "total": 6, 121 | "covered": 6, 122 | "skipped": 0, 123 | "pct": 100 124 | }, 125 | "branches": { 126 | "total": 0, 127 | "covered": 0, 128 | "skipped": 0, 129 | "pct": 100 130 | } 131 | }, 132 | "src/methods/stylesheets/base.ts": { 133 | "lines": { 134 | "total": 3, 135 | "covered": 3, 136 | "skipped": 0, 137 | "pct": 100 138 | }, 139 | "functions": { 140 | "total": 1, 141 | "covered": 1, 142 | "skipped": 0, 143 | "pct": 100 144 | }, 145 | "statements": { 146 | "total": 3, 147 | "covered": 3, 148 | "skipped": 0, 149 | "pct": 100 150 | }, 151 | "branches": { 152 | "total": 8, 153 | "covered": 8, 154 | "skipped": 0, 155 | "pct": 100 156 | } 157 | }, 158 | "src/methods/stylesheets/invert.ts": { 159 | "lines": { 160 | "total": 3, 161 | "covered": 3, 162 | "skipped": 0, 163 | "pct": 100 164 | }, 165 | "functions": { 166 | "total": 1, 167 | "covered": 1, 168 | "skipped": 0, 169 | "pct": 100 170 | }, 171 | "statements": { 172 | "total": 3, 173 | "covered": 3, 174 | "skipped": 0, 175 | "pct": 100 176 | }, 177 | "branches": { 178 | "total": 0, 179 | "covered": 0, 180 | "skipped": 0, 181 | "pct": 100 182 | } 183 | }, 184 | "src/methods/stylesheets/simple-css.ts": { 185 | "lines": { 186 | "total": 3, 187 | "covered": 3, 188 | "skipped": 0, 189 | "pct": 100 190 | }, 191 | "functions": { 192 | "total": 1, 193 | "covered": 1, 194 | "skipped": 0, 195 | "pct": 100 196 | }, 197 | "statements": { 198 | "total": 3, 199 | "covered": 3, 200 | "skipped": 0, 201 | "pct": 100 202 | }, 203 | "branches": { 204 | "total": 0, 205 | "covered": 0, 206 | "skipped": 0, 207 | "pct": 100 208 | } 209 | }, 210 | "src/methods/stylesheets/stylesheet-processor.ts": { 211 | "lines": { 212 | "total": 3, 213 | "covered": 3, 214 | "skipped": 0, 215 | "pct": 100 216 | }, 217 | "functions": { 218 | "total": 1, 219 | "covered": 1, 220 | "skipped": 0, 221 | "pct": 100 222 | }, 223 | "statements": { 224 | "total": 3, 225 | "covered": 3, 226 | "skipped": 0, 227 | "pct": 100 228 | }, 229 | "branches": { 230 | "total": 2, 231 | "covered": 2, 232 | "skipped": 0, 233 | "pct": 100 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/preferences/App.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
39 |

Options

40 | {#each current_preferences as pref (pref.name)} 41 | {#if pref.type !== 'configured_pages'} 42 |
43 |
44 | 47 |
48 | {#if pref.type === 'bool'} 49 |
50 | set_pref(pref.name, e.currentTarget.checked)} 53 | class="pref_{pref.name} full-width form-control" 54 | id="labeled_pref_{pref.name}" 55 | type="checkbox" 56 | data-pref-type={pref.type} 57 | /> 58 |
59 | {:else if pref.type === 'menulist'} 60 |
61 | 74 |
75 | {:else if pref.type === 'color'} 76 | set_pref(pref.name, e.detail.value)} 80 | class="col-xs-12 col-sm-8 col-md-6" 81 | /> 82 | {/if} 83 |
84 | 89 |
90 |
91 | {/if} 92 | {/each} 93 | 94 | {#await browser.runtime.getPlatformInfo() then platformInfo} 95 | {#if platformInfo.os !== 'android'} 96 |

Shortcuts

97 |
98 | In order to configure shortcuts, go to about:addons (Menu -> 99 | Add-ons), press on the cogwheel icon, then choose "Manage Extension 100 | Shortcuts" 101 |
102 | See this support article for the detais 106 | {/if} 107 | {/await} 108 | 109 |

Configured pages

110 | {#each Object.entries(configured_pages) as [page, method_index] (page)} 111 |
112 |
113 | {page} 114 |
115 |
116 | {methods[method_index].label} 117 |
118 |
119 | 129 |
130 |
131 | {:else} 132 |
133 |
There is no single configured page
134 |
135 | {/each} 136 |
137 | 138 | 140 | -------------------------------------------------------------------------------- /src/common/color_utils.ts: -------------------------------------------------------------------------------- 1 | import type { RGBA, RGB, HSL, HSV_obj, RGB_obj, DefaultColors } from './types'; 2 | import { get_acceptable_range } from './get_acceptable_range'; 3 | 4 | export function RGB_to_HSL(rgb_array: RGB): HSL { 5 | const R = (1.0 * rgb_array[0]) / 255; 6 | const G = (1.0 * rgb_array[1]) / 255; 7 | const B = (1.0 * rgb_array[2]) / 255; 8 | const MAX = Math.max(R, G, B); 9 | const MIN = Math.min(R, G, B); 10 | let H; 11 | if (MAX === MIN) { 12 | // H has no effect, set it to 0 to prevent undefined; 13 | H = 0; 14 | } else if (MAX === R && G >= B) { 15 | H = (60 * (G - B)) / (MAX - MIN); 16 | } else if (MAX === R && G < B) { 17 | H = (60 * (G - B)) / (MAX - MIN) + 360; 18 | } else if (MAX === G) { 19 | H = (60 * (B - R)) / (MAX - MIN) + 120; 20 | } else if (MAX === B) { 21 | H = (60 * (R - G)) / (MAX - MIN) + 240; 22 | } else { 23 | throw new Error('this code should have hever been reached'); 24 | } 25 | const L = (MAX + MIN) / 2; 26 | let S = (MAX - MIN) / (1 - Math.abs(1 - (MAX + MIN))); 27 | if (Number.isNaN(S)) { 28 | // isNaN is too slow 29 | S = 0; 30 | } 31 | return [H, S * 100, L * 100]; 32 | } 33 | 34 | export function RGB_to_HSV(R_: number, G_: number, B_: number): HSV_obj { 35 | const R = (1.0 * R_) / 255; 36 | const G = (1.0 * G_) / 255; 37 | const B = (1.0 * B_) / 255; 38 | 39 | const MAX = Math.max(R, G, B); 40 | const MIN = Math.min(R, G, B); 41 | 42 | let H: number; 43 | let S: number; 44 | const V: number = MAX; 45 | 46 | /* H */ 47 | if (MAX === MIN) { 48 | H = 0; 49 | } else if (MAX === R && G >= B) { 50 | H = 60 * ((G - B) / (MAX - MIN)); 51 | } else if (MAX === R && G < B) { 52 | H = 60 * ((G - B) / (MAX - MIN)) + 360; 53 | } else if (MAX === G) { 54 | H = 60 * ((B - R) / (MAX - MIN)) + 120; 55 | } else if (MAX === B) { 56 | H = 60 * ((R - G) / (MAX - MIN)) + 240; 57 | } else { 58 | throw new Error('this code should have hever been reached'); 59 | } 60 | 61 | /* S */ 62 | if (MAX === 0) { 63 | S = 0; 64 | } else { 65 | S = 1 - MIN / MAX; 66 | } 67 | 68 | return { H, S, V }; 69 | } 70 | 71 | export function HSV_to_RGB(H_: number, S_: number, V_: number): RGB_obj { 72 | const H = H_ * 1.0; 73 | const S = S_ * 100.0; 74 | const V = V_ * 100.0; 75 | 76 | const H_i = Math.floor(H / 60); 77 | const V_min = ((100 - S) * V) / 100; 78 | const a = (V - V_min) * (((H % 60) * 1.0) / 60); 79 | const V_inc = V_min + a; 80 | const V_dec = V - a; 81 | let R: number; 82 | let G: number; 83 | let B: number; 84 | 85 | switch (H_i) { 86 | case 0: 87 | R = V; 88 | G = V_inc; 89 | B = V_min; 90 | break; 91 | case 1: 92 | R = V_dec; 93 | G = V; 94 | B = V_min; 95 | break; 96 | case 2: 97 | R = V_min; 98 | G = V; 99 | B = V_inc; 100 | break; 101 | case 3: 102 | R = V_min; 103 | G = V_dec; 104 | B = V; 105 | break; 106 | case 4: 107 | R = V_inc; 108 | G = V_min; 109 | B = V; 110 | break; 111 | case 5: 112 | R = V; 113 | G = V_min; 114 | B = V_dec; 115 | break; 116 | default: 117 | throw new Error('bad H_i value'); 118 | } 119 | return { 120 | R: Math.floor(R * 2.55), 121 | G: Math.floor(G * 2.55), 122 | B: Math.floor(B * 2.55), 123 | }; 124 | } 125 | 126 | export function strip_alpha(rgba: RGBA): RGB { 127 | return rgba.slice(0, 3) as RGB; 128 | } 129 | 130 | export function lighten_or_darken_color( 131 | rgba_color_array: RGBA, 132 | darken_not_lighten: boolean, 133 | options: DefaultColors, 134 | ): string { 135 | const [H, S, L] = RGB_to_HSL(strip_alpha(rgba_color_array)); 136 | const alpha = rgba_color_array[3]; 137 | const range = get_acceptable_range(H); 138 | let new_L: number; 139 | if (S < 20) { 140 | return `rgba(${strip_alpha( 141 | darken_not_lighten 142 | ? options.default_dark_color 143 | : options.default_light_color, 144 | ).join(', ')}, ${alpha})`; 145 | } 146 | if (darken_not_lighten) { 147 | if (L <= range[0]) { 148 | new_L = L; 149 | } else if (L >= 100 - range[0]) { 150 | new_L = 100 - L; 151 | } else { 152 | new_L = range[0]; 153 | } 154 | } else { 155 | if (L >= range[1]) { 156 | new_L = L; 157 | } else if (L <= 100 - range[1]) { 158 | new_L = 100 - L; 159 | } else { 160 | new_L = range[1]; 161 | } 162 | } 163 | return `hsla(${H}, ${S}%, ${new_L}%, ${alpha})`; 164 | } 165 | 166 | export function lighten_color( 167 | rgba_color_array: RGBA, 168 | options: DefaultColors, 169 | ): string { 170 | return lighten_or_darken_color(rgba_color_array, false, options); 171 | } 172 | 173 | export function darken_color( 174 | rgba_color_array: RGBA, 175 | options: DefaultColors, 176 | ): string { 177 | return lighten_or_darken_color(rgba_color_array, true, options); 178 | } 179 | 180 | export function relative_luminance(color_array: RGB): number { 181 | const R = (1.0 * color_array[0]) / 255; 182 | const G = (1.0 * color_array[1]) / 255; 183 | const B = (1.0 * color_array[2]) / 255; 184 | // https://en.wikipedia.org/wiki/Luma_(video)#Luma_versus_relative_luminance 185 | // coefficients defined by Rec. 601 186 | return 0.299 * R + 0.587 * G + 0.114 * B; 187 | } 188 | -------------------------------------------------------------------------------- /src/common/generate-urls.ts: -------------------------------------------------------------------------------- 1 | export const hint_marker = ''; 2 | 3 | export function is_IPv4(maybe_ip: string): boolean { 4 | if (maybe_ip.length < 7 || maybe_ip.length > 15) { 5 | return false; 6 | } 7 | let number_of_octets = 0; 8 | let first: number | null = null; 9 | let second: number | null = null; 10 | let third: number | null = null; 11 | for (let i = 0; i <= maybe_ip.length; i++) { 12 | const code = maybe_ip.charCodeAt(i); 13 | if ( 14 | code === 46 /* . */ 15 | || i === maybe_ip.length /* last special iteration */ 16 | ) { 17 | number_of_octets++; 18 | if ( 19 | number_of_octets > 4 20 | || (number_of_octets < 4 && i === maybe_ip.length) 21 | ) { 22 | return false; 23 | } 24 | if (first === null) { 25 | return false; 26 | } 27 | if (third !== null && first * 100 + second! * 10 + third > 255) { 28 | return false; 29 | } 30 | first = null; 31 | second = null; 32 | third = null; 33 | continue; 34 | } else if (code < 48 /* 0 */ || code > 57 /* 9 */) { 35 | return false; 36 | } 37 | const digit = code - 48; 38 | if (first === null) { 39 | first = digit; 40 | continue; 41 | } else if (second === null) { 42 | second = digit; 43 | continue; 44 | } else if (third === null) { 45 | third = digit; 46 | continue; 47 | } else { 48 | return false; 49 | } 50 | } 51 | return true; 52 | } 53 | 54 | function split_domain_dumb(hostname: string): [string, string[]] { 55 | const splitted = hostname.split('.'); 56 | return [splitted.pop()!, splitted]; 57 | } 58 | 59 | export function generate_urls( 60 | url_str: string, 61 | hint: boolean = false, 62 | split_domain_func = split_domain_dumb, 63 | ): string[] { 64 | // This whole function is one of the most fragile pieces of code I've ever written, 65 | // touch it with great caution. Likely, there are unittests. 66 | try { 67 | const url_obj = new URL(url_str); 68 | const result_list: string[] = []; 69 | const protocol_real = url_obj.href.startsWith(`${url_obj.protocol}//`) 70 | ? `${url_obj.protocol}//` 71 | : url_obj.protocol; 72 | const is_http = 73 | url_obj.protocol === 'http:' || url_obj.protocol === 'https:'; 74 | let hint_added = false; 75 | if (hint && url_str.indexOf('/') < 0) { 76 | result_list.push(hint_marker); 77 | hint_added = true; 78 | } 79 | 80 | const pathname_parts = url_obj.pathname 81 | .split('/') 82 | .filter((p) => p.length > 0); 83 | const prepend_protocol_and_or_host = 84 | // eslint-disable-next-line no-nested-ternary 85 | url_obj.host 86 | ? `${is_http ? '' : protocol_real}${url_obj.host}/` 87 | : protocol_real.endsWith('//') 88 | ? `${protocol_real}/` 89 | : protocol_real; 90 | for (let i = pathname_parts.length - 1; i >= 0; i--) { 91 | result_list.push( 92 | `${prepend_protocol_and_or_host}${pathname_parts 93 | .slice(0, i + 1) 94 | .join('/')}`, 95 | ); 96 | } 97 | 98 | if (hint && !hint_added) { 99 | if (protocol_real === 'file://' && result_list.length > 0) { 100 | result_list.splice(result_list.length - 1, 0, hint_marker); 101 | } else { 102 | result_list.push(hint_marker); 103 | } 104 | hint_added = true; 105 | } 106 | if (url_obj.host && url_obj.hostname) { 107 | // host -> host:port 108 | // hostname -> host only 109 | if (url_obj.host !== url_obj.hostname) { 110 | // host:port if there is port 111 | // if there is no port, value will be added in the 112 | // next block (from tldts_obj) 113 | result_list.push( 114 | is_http ? url_obj.host : `${protocol_real}${url_obj.host}`, 115 | ); 116 | } 117 | } 118 | 119 | if (!is_IPv4(url_obj.hostname) && url_obj.hostname.indexOf('.') >= 0) { 120 | const [domain, subdomain_parts] = split_domain_func(url_obj.hostname); 121 | if (subdomain_parts) { 122 | for (let i = 0; i < subdomain_parts.length; i++) { 123 | result_list.push( 124 | `${subdomain_parts 125 | .slice(i, subdomain_parts.length) 126 | .join('.')}.${domain}`, 127 | ); 128 | } 129 | } 130 | result_list.push(is_http ? domain : `${protocol_real}${domain}`); 131 | } else if (url_obj.hostname) { 132 | result_list.push( 133 | is_http ? url_obj.hostname : `${protocol_real}${url_obj.hostname}`, 134 | ); 135 | } 136 | 137 | if (!is_http) { 138 | result_list.push(protocol_real); 139 | } 140 | 141 | /* istanbul ignore if: no idea how to reproduce it - 142 | it was added to tolerate unlikely failure */ 143 | if (result_list.length === 0) { 144 | console.error( 145 | `generate_urls: no urls has been generated, returning original: ${url_str}`, 146 | ); 147 | return hint ? [hint_marker, url_str] : [url_str]; 148 | } 149 | return result_list; 150 | } catch (e) { 151 | console.error( 152 | `generate_urls: something went wrong, returning original URL: ${url_str}`, 153 | e, 154 | ); 155 | // if something goes horribly wrong, return at least the original URL 156 | return hint ? [hint_marker, url_str] : [url_str]; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/common/shared.ts: -------------------------------------------------------------------------------- 1 | import type { Browser, Storage } from 'webextension-polyfill'; 2 | import type { 3 | Preferences, 4 | PrefsType, 5 | ConfiguredPages, 6 | MethodIndex, 7 | BoolPreference, 8 | MenuListPreference, 9 | ColorPreference, 10 | ConfiguredPagesPreference, 11 | } from './types'; 12 | import { methods } from '../methods/methods'; 13 | 14 | declare const browser: Browser; 15 | 16 | export const preferences: Preferences = [ 17 | { 18 | type: 'bool', 19 | name: 'enabled', 20 | value: true, 21 | title: 'Enabled', 22 | } as BoolPreference, 23 | { 24 | title: 'Default method of changing page colors', 25 | value: 1, 26 | type: 'menulist', 27 | options: Object.keys(methods) 28 | .filter((key) => parseInt(key, 10) >= 0) 29 | .map((key) => ({ 30 | label: methods[key].label, 31 | value: key, 32 | })), 33 | name: 'default_method', 34 | } as MenuListPreference, 35 | { 36 | type: 'color', 37 | name: 'default_foreground_color', 38 | value: '#ffffff', 39 | title: 'Default foreground color', 40 | } as ColorPreference, 41 | { 42 | type: 'color', 43 | name: 'default_background_color', 44 | value: '#000000', 45 | title: 'Default background color', 46 | } as ColorPreference, 47 | { 48 | type: 'color', 49 | name: 'default_link_color', 50 | value: '#7fd7ff', 51 | title: 'Default link color', 52 | } as ColorPreference, 53 | { 54 | type: 'color', 55 | name: 'default_visited_color', 56 | value: '#ffafff', 57 | title: 'Default visited link color', 58 | } as ColorPreference, 59 | { 60 | type: 'color', 61 | name: 'default_active_color', 62 | value: '#ff0000', 63 | title: 'Default active link color', 64 | } as ColorPreference, 65 | { 66 | type: 'color', 67 | name: 'default_selection_color', 68 | value: '#8080ff', 69 | title: 'Default selection color', 70 | } as ColorPreference, 71 | { 72 | type: 'bool', 73 | name: 'do_not_set_overrideDocumentColors_to_never', 74 | value: false, 75 | title: 'Do not set "Override Document Colors" to "never" (not recommended)', 76 | } as BoolPreference, 77 | { 78 | type: 'configured_pages', 79 | name: 'configured_pages', 80 | value: {}, 81 | title: 'configured_pages', 82 | } as ConfiguredPagesPreference, 83 | ]; 84 | 85 | export interface PrefsWithValues { 86 | [key: string]: PrefsType; 87 | } 88 | export const prefs_keys_with_defaults = ((): PrefsWithValues => { 89 | const result: PrefsWithValues = {}; 90 | preferences.forEach((pref) => { 91 | result[pref.name] = pref.value; 92 | }); 93 | return result; 94 | })(); 95 | 96 | export function get_prefs(prefs?: string[]): Promise; 97 | export function get_prefs(prefs: 'enabled'): Promise; 98 | export function get_prefs(prefs: 'configured_pages'): Promise; 99 | export function get_prefs(prefs: 'default_method'): Promise; 100 | export function get_prefs( 101 | prefs: 'do_not_set_overrideDocumentColors_to_never', 102 | ): Promise; 103 | export function get_prefs(prefs: string): Promise; 104 | export async function get_prefs( 105 | prefs?: string | string[], 106 | ): Promise { 107 | let query: PrefsWithValues = {}; 108 | let is_single = false; 109 | if (Array.isArray(prefs)) { 110 | query = {}; 111 | for (const key of prefs) { 112 | query[key] = prefs_keys_with_defaults[key]; 113 | } 114 | } else if (Object.prototype.toString.call(prefs) === '[object String]') { 115 | query = { [prefs as string]: prefs_keys_with_defaults[prefs as string] }; 116 | is_single = true; 117 | } else if (prefs === undefined || prefs === null) { 118 | query = prefs_keys_with_defaults; 119 | } else { 120 | throw new Error('get_prefs parameter has unsupported type'); 121 | } 122 | const ret_data = await browser.storage.local.get(query); 123 | return is_single ? ret_data[prefs as string] : ret_data; 124 | } 125 | 126 | export function set_pref(pref: string, value: PrefsType): Promise { 127 | if (prefs_keys_with_defaults[pref] === value) { 128 | return browser.storage.local.remove(pref); 129 | } else { 130 | return browser.storage.local.set({ [pref]: value }); 131 | } 132 | } 133 | 134 | export function on_prefs_change( 135 | callback: (changes: { [s: string]: Storage.StorageChange }) => void, 136 | ) { 137 | browser.storage.onChanged.addListener((changes, areaName) => { 138 | if (areaName !== 'local') { 139 | throw new Error('unsupported'); 140 | } 141 | for (const pref of Object.keys(changes)) { 142 | // if option has been removed, it means that it's value has been set to default 143 | if (!Object.prototype.hasOwnProperty.call(changes[pref], 'newValue')) { 144 | // eslint-disable-next-line no-param-reassign 145 | changes[pref].newValue = prefs_keys_with_defaults[pref]; 146 | } 147 | } 148 | callback(changes); 149 | }); 150 | } 151 | 152 | export async function get_merged_configured_common( 153 | get_configured_private: () => Promise, 154 | ): Promise { 155 | const local_storage_p = browser.storage.local.get({ configured_pages: {} }); 156 | return { 157 | ...(await local_storage_p).configured_pages, 158 | ...(await get_configured_private()), 159 | // ...built_in_configured, 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /src/methods/stylesheets/base.ts: -------------------------------------------------------------------------------- 1 | import type { RenderOptions } from '../../common/types'; 2 | 3 | export const name = 'base'; 4 | export function render({ 5 | default_foreground_color, 6 | default_background_color, 7 | default_link_color, 8 | default_visited_color, 9 | default_active_color, 10 | default_selection_color, 11 | is_toplevel, 12 | is_darkbg, 13 | }: RenderOptions): string { 14 | return ` 15 | :root { 16 | --dark-background-light-text-add-on-foreground-color: ${default_foreground_color} !important; 17 | --dark-background-light-text-add-on-background-color: ${default_background_color} !important; 18 | --dark-background-light-text-add-on-link-color: ${default_link_color} !important; 19 | --dark-background-light-text-add-on-visited-color: ${default_visited_color} !important; 20 | --dark-background-light-text-add-on-active-color: ${default_active_color} !important; 21 | --dark-background-light-text-add-on-selection-color: ${default_selection_color} !important; 22 | } 23 | 24 | html { 25 | ${ 26 | is_toplevel 27 | ? `\ 28 | background-color: ${default_background_color}; 29 | ` 30 | : '' 31 | }\ 32 | color: ${default_foreground_color}; 33 | } 34 | 35 | *:link, 36 | *:link * { 37 | color: ${default_link_color} !important; 38 | } 39 | 40 | *:visited, 41 | *:visited * { 42 | color: ${default_visited_color} !important; 43 | } 44 | 45 | input[type="range"] { 46 | -moz-appearance: none; 47 | } 48 | 49 | button, 50 | input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]), 51 | textarea, 52 | select, 53 | [contenteditable="true"] { 54 | -moz-appearance: none !important; 55 | color: ${default_foreground_color} !important; 56 | background-color: ${default_background_color}; 57 | border-radius: 4px; 58 | border-width: 1px; 59 | border-color: ${default_foreground_color}; 60 | border-style: solid; 61 | transition-duration: 0.3s; 62 | transition-property: border-color, box-shadow; 63 | } 64 | 65 | input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="button"]):not([type="color"]):not([type="image"]):not([type="reset"]):not([type="submit"]), 66 | textarea, 67 | [contenteditable="true"] { 68 | background-image: none !important; 69 | } 70 | 71 | input:focus:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="button"]):not([type="color"]):not([type="image"]):not([type="reset"]):not([type="submit"]), 72 | textarea:focus, 73 | [contenteditable="true"]:focus { 74 | box-shadow: inset 0 0 0.15em 0.15em ${default_selection_color} !important; 75 | border-color: ${default_selection_color} !important; 76 | } 77 | 78 | button, 79 | input[type="button"], 80 | input[type="color"], 81 | input[type="image"], 82 | input[type="reset"], 83 | input[type="submit"], 84 | select { 85 | box-shadow: 0 0 0.15em 0.15em transparent !important; 86 | } 87 | 88 | button:focus, 89 | input[type="button"]:focus, 90 | input[type="color"]:focus, 91 | input[type="image"]:focus, 92 | input[type="reset"]:focus, 93 | input[type="submit"]:focus, 94 | select:focus { 95 | box-shadow: 0 0 0.15em 0.15em ${default_selection_color} !important; 96 | border-color: ${default_selection_color} !important; 97 | } 98 | 99 | select { 100 | background-image: url('data:image/svg+xml;utf8,') !important; 103 | background-position: right center !important; 104 | background-repeat: no-repeat !important; 105 | padding-right: 1em !important; 106 | background-size: 1em !important; 107 | } 108 | 109 | *::-moz-selection { 110 | color: ${default_foreground_color} !important; 111 | background: ${default_selection_color} !important; 112 | text-shadow: 113 | ${default_background_color} 0 0 1pt, 114 | ${default_background_color} 0 0 2pt, 115 | ${default_background_color} 0 0 3pt, 116 | ${default_background_color} 0 0 4pt, 117 | ${default_background_color} 0 0 5pt, 118 | ${default_background_color} 0 0 5pt, 119 | ${default_background_color} 0 0 5pt !important; 120 | } 121 | 122 | ${'' /* TODO: "black on transparent" mark */}\ 123 | ${ 124 | is_darkbg 125 | ? `\ 126 | img[alt="inline_formula"], 127 | .mwe-math-fallback-image-inline, 128 | ${ 129 | '' /* charts, for example on https://addons.mozilla.org/en-US/firefox/addon/black-background-white-text/statistics/ */ 130 | }\ 131 | .highcharts-container { 132 | filter: invert(1) hue-rotate(180deg) !important; 133 | } 134 | ` 135 | : '' 136 | }\ 137 | \ 138 | ${'' /* https://catalog.onliner.by/ */}\ 139 | ${ 140 | is_darkbg 141 | ? `\ 142 | .catalog-content .i-checkbox__faux::before { 143 | filter: invert(1); 144 | } 145 | ` 146 | : '' 147 | }\ 148 | \ 149 | ${'' /* #8 google scholar bars on right sidebar */}\ 150 | #gs_bdy .gsc_g_a[style*="height"] { 151 | background-color: rgb(119 119 119) !important; 152 | } 153 | 154 | ${ 155 | '' /* https://github.com/qooob/authentic-theme radio buttons. unfortunately, there is no public available demo */ 156 | }\ 157 | .awradio label::after { 158 | background-color: ${default_foreground_color} !important; 159 | } 160 | \ 161 | ${'' /* buttons on many google services (Books, Translate, etc) */}\ 162 | ${ 163 | is_darkbg 164 | ? `\ 165 | .jfk-button-img { 166 | filter: invert(1); 167 | } 168 | ` 169 | : '' 170 | }\ 171 | 172 | ${'' /* Google Docs cursor (#220) */}\ 173 | #kix-current-user-cursor-caret[style*="border-color: rgb(0 0 0)"] { 174 | border-color: ${default_foreground_color} !important; 175 | } 176 | `; 177 | } 178 | -------------------------------------------------------------------------------- /test/test-background-lib.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { describe, it } from 'mocha'; 3 | import type { WebRequest } from 'webextension-polyfill'; 4 | import { readFileSync } from 'fs'; 5 | import { modify_csp, modify_cors, version_lt } from '../src/background/lib'; 6 | 7 | const csp_test_data = [ 8 | ['frame-src whatever', 'frame-src whatever'], 9 | 10 | ["style-src 'none'", "style-src 'unsafe-inline'"], 11 | [ 12 | 'style-src https://example.com', 13 | "style-src https://example.com 'unsafe-inline'", 14 | ], 15 | ["style-src 'unsafe-inline'", "style-src 'unsafe-inline'"], 16 | ['style-src data:', "style-src data: 'unsafe-inline'"], 17 | [ 18 | 'style-src https://example.com data:', 19 | "style-src https://example.com data: 'unsafe-inline'", 20 | ], 21 | [ 22 | "style-src https://example.com 'unsafe-inline'", 23 | "style-src https://example.com 'unsafe-inline'", 24 | ], 25 | 26 | ["default-src 'unsafe-inline'", "default-src 'unsafe-inline'"], 27 | [ 28 | "default-src https://example.com 'unsafe-inline'", 29 | "default-src https://example.com 'unsafe-inline'", 30 | ], 31 | 32 | ["default-src 'none'", "default-src 'none'; style-src 'unsafe-inline'"], 33 | [ 34 | 'default-src https://example.com', 35 | "default-src https://example.com; style-src https://example.com 'unsafe-inline'", 36 | ], 37 | ]; 38 | 39 | describe('test modify CSP', () => { 40 | csp_test_data.forEach(([src, expected]) => { 41 | it(src, () => { 42 | const result = modify_csp({ 43 | name: 'Content-Security-Policy', 44 | value: src, 45 | }); 46 | assert.equal(result.value, expected); 47 | }); 48 | }); 49 | it('other header', () => { 50 | const header = { 51 | name: 'h9U4yPiZTzYTfiHZgSWk', 52 | value: '¤frÃ;åѯNzô+ Õ¤&§¹öö±H6ÍWiØa (Ì^nD$i+ösâ[*', 53 | }; 54 | assert.equal(modify_csp(header), header); 55 | }); 56 | }); 57 | 58 | const cors_test_data: Array< 59 | [ 60 | string, 61 | WebRequest.HttpHeaders, 62 | { documentUrl?: string }, 63 | WebRequest.HttpHeaders, 64 | ] 65 | > = [ 66 | [ 67 | 'no documentUrl', 68 | [{ name: 'Some-Random-Header', value: 'Some value' }], 69 | { 70 | documentUrl: undefined, 71 | }, 72 | [{ name: 'Some-Random-Header', value: 'Some value' }], 73 | ], 74 | [ 75 | 'just add Access-Control-Allow-Origin', 76 | [{ name: 'Some-Random-Header', value: 'Some value' }], 77 | { 78 | documentUrl: 'https://example.com/example', 79 | }, 80 | [ 81 | { name: 'Some-Random-Header', value: 'Some value' }, 82 | { name: 'Access-Control-Allow-Origin', value: 'https://example.com' }, 83 | ], 84 | ], 85 | [ 86 | 'do nothing', 87 | [ 88 | { name: 'Some-Random-Header', value: 'Some value' }, 89 | { name: 'Access-Control-Allow-Origin', value: 'https://example.com' }, 90 | { name: 'Some-Another-Random-Header', value: 'Some value' }, 91 | ], 92 | { 93 | documentUrl: 'https://example.com/example', 94 | }, 95 | [ 96 | { name: 'Some-Random-Header', value: 'Some value' }, 97 | { name: 'Access-Control-Allow-Origin', value: 'https://example.com' }, 98 | { name: 'Some-Another-Random-Header', value: 'Some value' }, 99 | ], 100 | ], 101 | [ 102 | 'modify', 103 | [ 104 | { name: 'Some-Random-Header', value: 'Some value' }, 105 | { name: 'Access-Control-Allow-Origin', value: 'https://example.org' }, 106 | { name: 'Some-Another-Random-Header', value: 'Some value' }, 107 | ], 108 | { 109 | documentUrl: 'https://example.com/example', 110 | }, 111 | [ 112 | { name: 'Some-Random-Header', value: 'Some value' }, 113 | { name: 'Access-Control-Allow-Origin', value: 'https://example.com' }, 114 | { name: 'Some-Another-Random-Header', value: 'Some value' }, 115 | ], 116 | ], 117 | [ 118 | 'modify lowercase', 119 | [ 120 | { name: 'Some-Random-Header', value: 'Some value' }, 121 | { name: 'access-control-allow-origin', value: 'https://example.org' }, 122 | { name: 'Some-Another-Random-Header', value: 'Some value' }, 123 | ], 124 | { 125 | documentUrl: 'https://example.com/example', 126 | }, 127 | [ 128 | { name: 'Some-Random-Header', value: 'Some value' }, 129 | { name: 'access-control-allow-origin', value: 'https://example.com' }, 130 | { name: 'Some-Another-Random-Header', value: 'Some value' }, 131 | ], 132 | ], 133 | ]; 134 | 135 | describe('test modify Access-Control-Allow-Origin', () => { 136 | cors_test_data.forEach(([name, src, details, expected]) => { 137 | it(name, () => { 138 | assert.deepEqual( 139 | modify_cors(src, details as WebRequest.OnHeadersReceivedDetailsType), 140 | expected, 141 | ); 142 | }); 143 | }); 144 | }); 145 | 146 | const test_versions: Array<[string, string, boolean]> = [ 147 | ['1.0.0', '2.0.0', true], 148 | ['1.0.0', '1.1.0', true], 149 | ['1.1.0', '1.1.1', true], 150 | 151 | ['1.2.3', '1.2.3', false], 152 | 153 | ['1.0.0', '0.1.1', false], 154 | ['1.1.0', '1.0.1', false], 155 | ['1.1.2', '1.1.1', false], 156 | 157 | ['1.0', '0.1.1', false], 158 | ['1.0', '1.0.1', true], 159 | ['1.0.1.1', '1.0.1', false], 160 | ['1.0.1.0', '1.0.1', false], 161 | ['1.0.0.1', '1.0.1', true], 162 | ]; 163 | 164 | describe('test version_lt', () => { 165 | test_versions.forEach(([target, ref, expected_result]) => { 166 | it(`${target} ${expected_result ? '<' : '>='} ${ref}`, () => { 167 | assert.equal(version_lt(target, ref), expected_result); 168 | }); 169 | }); 170 | const current_ver = JSON.parse( 171 | readFileSync('./manifest.json', 'utf-8'), 172 | ).version; 173 | it(`0.1.0 < ${current_ver} (current version)`, () => { 174 | assert.equal(version_lt('0.1.0', current_ver), true); 175 | }); 176 | it('ensure that current version is parseable by version_lt', () => { 177 | assert( 178 | /^[0-9.]+$/.test(current_ver), 179 | 'version_lt() only handles simple versions (i. e. dot-separated numbers). If you want to use alpha, beta, etc versions, you better use semver library.', 180 | ); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /src/content/index.ts: -------------------------------------------------------------------------------- 1 | import type { Browser, Storage } from 'webextension-polyfill'; 2 | import type { 3 | AddonOptions, 4 | ConfiguredPages, 5 | ConfiguredTabs, 6 | MethodIndex, 7 | MethodExecutor, 8 | MethodMetadataWithExecutors, 9 | } from '../common/types'; 10 | import { methods } from '../methods/methods-with-executors'; 11 | import { generate_urls } from '../common/generate-urls'; 12 | 13 | declare const browser: Browser; 14 | 15 | const tabId_promise = browser.runtime.sendMessage({ action: 'query_tabId' }); 16 | let is_iframe: boolean; 17 | try { 18 | is_iframe = window.self !== window.top; 19 | } catch (e) { 20 | is_iframe = true; 21 | } 22 | 23 | declare global { 24 | interface Window { 25 | content_script_state: 26 | | 'normal_order' 27 | | 'registered_content_script_first' 28 | | 'does not matters anymore' 29 | | undefined; 30 | prefs: AddonOptions; 31 | merged_configured: ConfiguredPages; 32 | configured_tabs: ConfiguredTabs; 33 | rendered_stylesheets: { [key: string]: string }; 34 | do_it: (changes: { [s: string]: Storage.StorageChange }) => Promise; 35 | } 36 | } 37 | 38 | // @ts-ignore: 2454 39 | if (typeof window.content_script_state === 'undefined') { 40 | /* #226 part 1 workaround */ 41 | window.content_script_state = 'normal_order'; 42 | } 43 | 44 | async function get_method_for_url( 45 | url: string, 46 | ): Promise { 47 | if (window.prefs.enabled) { 48 | if (is_iframe) { 49 | const parent_method_number = await browser.runtime.sendMessage({ 50 | action: 'query_parent_method_number', 51 | }); 52 | if (methods[parent_method_number].affects_iframes) { 53 | return methods[0]; 54 | } else if (url === 'about:blank' || url === 'about:srcdoc') { 55 | return methods[parent_method_number]; 56 | } 57 | } 58 | // TODO: get rid of await here, https://bugzilla.mozilla.org/show_bug.cgi?id=1574713 59 | let tab_configuration: MethodIndex | boolean = false; 60 | if (Object.keys(window.configured_tabs).length > 0) { 61 | const tabId = await tabId_promise; 62 | tab_configuration = Object.prototype.hasOwnProperty.call( 63 | window.configured_tabs, 64 | tabId, 65 | ) 66 | ? window.configured_tabs[tabId] 67 | : false; 68 | } 69 | if (tab_configuration !== false) { 70 | return methods[tab_configuration]; 71 | } 72 | 73 | const configured_urls = Object.keys(window.merged_configured); 74 | for (const gen_url of generate_urls(url)) { 75 | if (configured_urls.indexOf(gen_url) >= 0) { 76 | return methods[window.merged_configured[gen_url]]; 77 | } 78 | } 79 | return methods[window.prefs.default_method]; 80 | } else { 81 | return methods[0]; 82 | } 83 | } 84 | 85 | let current_method: MethodMetadataWithExecutors; 86 | let resolve_current_method_promise: 87 | | ((mmd: MethodMetadataWithExecutors) => void) 88 | | null; 89 | let current_method_promise: Promise = new Promise( 90 | (resolve: (mmd: MethodMetadataWithExecutors) => void) => { 91 | resolve_current_method_promise = resolve; 92 | }, 93 | ); 94 | let current_method_executor: MethodExecutor | undefined; 95 | window.do_it = async function do_it(changes: { 96 | [s: string]: Storage.StorageChange; 97 | }) { 98 | try { 99 | const new_method = await get_method_for_url(window.document.documentURI); 100 | if (resolve_current_method_promise) { 101 | resolve_current_method_promise(new_method); 102 | resolve_current_method_promise = null; 103 | } else { 104 | current_method_promise = Promise.resolve(new_method); 105 | } 106 | if ( 107 | !current_method 108 | || new_method.number !== current_method.number 109 | || Object.keys(changes).some((key) => key.indexOf('_color') >= 0) // TODO: better condition 110 | ) { 111 | for (const node of document.querySelectorAll( 112 | 'style[class="dblt-ykjmwcnxmi"]', 113 | )) { 114 | node.parentElement!.removeChild(node); 115 | } 116 | for (const css_renderer of new_method.stylesheets) { 117 | const style_node = document.createElement('style'); 118 | style_node.setAttribute('data-source', css_renderer.name); 119 | style_node.classList.add('dblt-ykjmwcnxmi'); 120 | style_node.innerText = 121 | window.rendered_stylesheets[ 122 | `${css_renderer.name}_${is_iframe ? 'iframe' : 'toplevel'}` 123 | ]; 124 | document.documentElement.appendChild(style_node); 125 | if (!document.body) { 126 | // this should move our element after 127 | // which is important in specificity fight 128 | document.addEventListener('DOMContentLoaded', () => { 129 | document.documentElement.appendChild(style_node); 130 | }); 131 | } 132 | } 133 | if (current_method_executor) { 134 | current_method_executor.unload_from_window(); 135 | current_method_executor = undefined; 136 | } 137 | if (new_method.executor) { 138 | // eslint-disable-next-line new-cap 139 | current_method_executor = new new_method.executor(window, window.prefs); 140 | current_method_executor.load_into_window(); 141 | } 142 | } 143 | current_method = new_method; 144 | } catch (e) { 145 | console.error(e); 146 | } 147 | }; 148 | 149 | interface GetMethodNumberMsg { 150 | action: 'get_method_number'; 151 | } 152 | browser.runtime.onMessage.addListener( 153 | async (message: GetMethodNumberMsg, _sender) => { 154 | try { 155 | // TODO: statically typed runtime.onMessage 156 | if (!message.action) { 157 | console.error('bad message!', message); 158 | return; 159 | } 160 | switch (message.action) { 161 | case 'get_method_number': 162 | return (await current_method_promise).number; 163 | default: 164 | console.error('bad message 2!', message); 165 | return; 166 | } 167 | } catch (e) { 168 | console.error(e); 169 | } 170 | return; 171 | }, 172 | ); 173 | 174 | if (window.content_script_state === 'registered_content_script_first') { 175 | /* #226 part 1 workaround */ 176 | window.do_it({}); 177 | window.content_script_state = 'does not matters anymore'; 178 | } 179 | -------------------------------------------------------------------------------- /src/preferences/ColorInput.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 246 | 247 |
248 | emit(false)} /> 249 | emit(true)} 253 | class={text_class} 254 | list="css-colors" 255 | on:animationend={() => (shake_it = false)} 256 | /> 257 | {#if local_i === 0} 258 | 259 | {#each css_keywords as kw} 260 | 262 | {/if} 263 |
264 | 265 | 321 | -------------------------------------------------------------------------------- /src/browser-action/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | import type { Browser } from 'webextension-polyfill'; 3 | import { 4 | get_merged_configured_common, 5 | get_prefs, 6 | set_pref, 7 | } from '../common/shared'; 8 | import { methods } from '../methods/methods'; 9 | import { hint_marker } from '../common/generate-urls'; 10 | import { smart_generate_urls } from '../common/smart-generate-urls'; 11 | import type { ConfiguredPages } from '../common/types'; 12 | import '../common/ui-style'; 13 | 14 | declare const browser: Browser; 15 | 16 | (async () => { 17 | function get_merged_configured(): Promise { 18 | return get_merged_configured_common(() => 19 | browser.runtime.sendMessage({ action: 'get_configured_private' }), 20 | ); 21 | } 22 | async function generate_urls_with_preselect_from_configured( 23 | url_str: string, 24 | ): Promise<{ list: string[]; preselect?: string }> { 25 | let result_list = smart_generate_urls(url_str, true); 26 | let preselect: string | undefined; 27 | 28 | const merged = await get_merged_configured(); 29 | for (const url of result_list) { 30 | if (url === hint_marker) { 31 | continue; 32 | } 33 | if (url in merged) { 34 | preselect = url; 35 | break; 36 | } 37 | } 38 | if (!preselect) { 39 | let next_is_preselected = false; 40 | for (const url of result_list) { 41 | if (url === hint_marker) { 42 | next_is_preselected = true; 43 | continue; 44 | } 45 | if (next_is_preselected) { 46 | preselect = url; 47 | break; 48 | } 49 | } 50 | } 51 | result_list = result_list.filter((url) => url !== hint_marker).reverse(); 52 | 53 | return { list: result_list, preselect }; 54 | } 55 | 56 | const CURRENT_TAB_LABEL = '< Current Tab >'; 57 | const current_tab = ( 58 | await browser.tabs.query({ 59 | // popup in the new Fenix is now in a separate window 60 | currentWindow: 61 | (await browser.runtime.getPlatformInfo()).os === 'android' 62 | ? undefined 63 | : true, 64 | active: true, 65 | }) 66 | )[0]; 67 | const url = current_tab.url!; 68 | 69 | function close() { 70 | window.close(); // works for pop-up on desktop 71 | if (!current_tab.active) { 72 | // this is the case for Fennec where pop-up is actually a tab 73 | // activating any tab other than our fake pop-up will close pop-up 74 | browser.tabs.update(current_tab.id, { active: true }); 75 | } 76 | } 77 | 78 | let message: string | boolean = false; 79 | try { 80 | await browser.tabs.executeScript(current_tab.id, { 81 | code: '{}', 82 | runAt: 'document_start', 83 | }); 84 | } catch (e) { 85 | message = `Modification of this page is not available due to ${ 86 | (await browser.runtime.getBrowserInfo()).name 87 | } restrictions`; 88 | } 89 | if (!message) { 90 | if (url.indexOf(browser.runtime.getURL('/')) === 0) { 91 | message = "Extension's own internal pages are already well configured"; 92 | } 93 | } 94 | 95 | const configured = await get_merged_configured(); 96 | // eslint-disable-next-line prefer-const 97 | let { preselect, list: urls } = 98 | await generate_urls_with_preselect_from_configured(url); 99 | const current_url_method = await browser.runtime.sendMessage({ 100 | action: 'get_tab_configuration', 101 | tab_id: current_tab.id, 102 | }); 103 | if (current_url_method) { 104 | preselect = CURRENT_TAB_LABEL; 105 | } 106 | const isPrivate = current_tab.incognito; 107 | const enabled = await get_prefs('enabled'); 108 | const body = document.querySelector('body')!; 109 | if ((await browser.runtime.getPlatformInfo()).os === 'android') { 110 | body.setAttribute('class', 'touchscreen'); 111 | } 112 | 113 | while (body.firstChild) { 114 | body.removeChild(body.firstChild); 115 | } 116 | 117 | async function handle_choose_url() { 118 | const url = (document.querySelector('#url_select') as HTMLFormElement) 119 | .value; 120 | let current_url_method; 121 | if (url === CURRENT_TAB_LABEL) { 122 | current_url_method = await browser.runtime.sendMessage({ 123 | action: 'get_tab_configuration', 124 | tab_id: current_tab.id, 125 | }); 126 | } else { 127 | current_url_method = 128 | configured[ 129 | (document.querySelector('#url_select') as HTMLFormElement).value 130 | ]; 131 | } 132 | if (current_url_method) { 133 | ( 134 | document.querySelector( 135 | `#method_${current_url_method}`, 136 | ) as HTMLFormElement 137 | ).checked = true; 138 | } else { 139 | (document.querySelector('#method_-1') as HTMLFormElement).checked = true; 140 | } 141 | } 142 | 143 | async function handle_method_change() { 144 | const methods = document.querySelectorAll( 145 | 'input.methods', 146 | ) as NodeListOf; 147 | let checked_method: HTMLFormElement; 148 | for (let i = 0; i < methods.length; ++i) { 149 | if (methods[i].checked) { 150 | checked_method = methods[i]; 151 | break; 152 | } 153 | } 154 | const method_n = checked_method!.value; 155 | const url: string = ( 156 | document.querySelector('#url_select') as HTMLFormElement 157 | ).value; 158 | 159 | if (url === CURRENT_TAB_LABEL) { 160 | browser.runtime.sendMessage({ 161 | action: 'set_configured_tab', 162 | key: current_tab.id, 163 | value: method_n >= 0 ? method_n : null, 164 | }); 165 | } else if (isPrivate) { 166 | browser.runtime.sendMessage({ 167 | action: 'set_configured_private', 168 | key: url, 169 | value: method_n >= 0 ? method_n : null, 170 | }); 171 | } else { 172 | const configured_pages = await get_prefs('configured_pages'); 173 | if (method_n < 0) { 174 | delete configured_pages[url]; 175 | } else { 176 | configured_pages[url] = method_n; 177 | } 178 | await set_pref('configured_pages', configured_pages); 179 | } 180 | close(); 181 | } 182 | 183 | const checkbox_label = document.createElement('label'); 184 | checkbox_label.setAttribute('class', 'enabled_label'); 185 | checkbox_label.textContent = 'Enabled'; 186 | const checkbox = document.createElement('input'); 187 | checkbox.setAttribute('type', 'checkbox'); 188 | checkbox.checked = enabled; 189 | checkbox.onchange = () => { 190 | set_pref('enabled', checkbox.checked).then(() => close()); 191 | }; 192 | checkbox_label.appendChild(checkbox); 193 | body.appendChild(checkbox_label); 194 | 195 | body.appendChild(document.createElement('hr')); 196 | 197 | const container = document.createElement('div'); 198 | container.setAttribute('class', 'page_settings_container'); 199 | container.style.position = 'relative'; 200 | 201 | if (!enabled) { 202 | const overlay = document.createElement('div'); 203 | overlay.setAttribute('class', 'disabled_overlay'); 204 | container.appendChild(overlay); 205 | } 206 | if (message) { 207 | const msg = document.createElement('div'); 208 | msg.textContent = message; 209 | msg.setAttribute('class', 'error_msg'); 210 | container.appendChild(msg); 211 | } else { 212 | const title = document.createElement('div'); 213 | title.textContent = 'Dark Background and Light Text options for:'; 214 | title.setAttribute('class', 'options_for'); 215 | container.appendChild(title); 216 | const select = document.createElement('select'); 217 | select.id = 'url_select'; 218 | select.onchange = handle_choose_url; 219 | urls.push(CURRENT_TAB_LABEL); 220 | for (const url of urls) { 221 | const option = document.createElement('option'); 222 | option.textContent = url; 223 | if (url === preselect) { 224 | option.setAttribute('selected', 'true'); 225 | } 226 | select.appendChild(option); 227 | } 228 | container.appendChild(select); 229 | if (isPrivate) { 230 | const private_note = document.createElement('div'); 231 | private_note.textContent = 232 | 'Note: this settings will not be saved for private tabs.'; 233 | container.appendChild(private_note); 234 | } 235 | const form_methods = document.createElement('form'); 236 | const ul_methods = document.createElement('ul'); 237 | form_methods.appendChild(ul_methods); 238 | 239 | for (const method of Object.keys(methods)) { 240 | if (parseInt(method, 10) > -5) { 241 | // TODO: document it somehow? (or remove?) 242 | const li = document.createElement('li'); 243 | const input = document.createElement('input'); 244 | const label = document.createElement('span'); 245 | const label_click = document.createElement('label'); 246 | input.type = 'radio'; 247 | input.name = 'method'; 248 | input.value = methods[method].number; 249 | input.id = `method_${methods[method].number}`; 250 | input.className = 'methods'; 251 | label.textContent = methods[method].label; 252 | label.setAttribute('class', 'label_no_click'); 253 | label_click.setAttribute('for', input.id); 254 | label_click.setAttribute('class', 'label_click_workaround'); 255 | li.appendChild(label_click); 256 | li.appendChild(input); 257 | li.appendChild(label); 258 | input.onchange = handle_method_change; 259 | ul_methods.appendChild(li); 260 | } 261 | } 262 | container.appendChild(form_methods); 263 | } 264 | body.appendChild(container); 265 | if (!message) { 266 | handle_choose_url(); 267 | } 268 | 269 | const preferences = document.createElement('div'); 270 | const preferences_note = document.createTextNode( 271 | 'Configure colors, "Default" behaviour and more here: ', 272 | ); 273 | preferences.appendChild(preferences_note); 274 | 275 | const prefs_button = document.createElement('button'); 276 | prefs_button.setAttribute('icon', 'properties'); 277 | prefs_button.textContent = 'Global Preferences'; 278 | prefs_button.onclick = () => { 279 | /* see bug 1414917 */ browser.runtime.sendMessage({ 280 | action: 'open_options_page', 281 | }); 282 | close(); 283 | }; 284 | preferences.appendChild(prefs_button); 285 | 286 | body.appendChild(preferences); 287 | })().catch((rejection) => console.error(rejection)); 288 | -------------------------------------------------------------------------------- /test/test-generate-urls.ts: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual, strict as assert } from 'assert'; 2 | import { describe } from 'mocha'; 3 | import { 4 | generate_urls, 5 | hint_marker, 6 | is_IPv4, 7 | } from '../src/common/generate-urls'; 8 | import { smart_generate_urls } from '../src/common/smart-generate-urls'; 9 | 10 | const DUMB_DOMAIN_DETECTION = '__DUMB__'; 11 | const test_urls = { 12 | 'about:config': [hint_marker, 'about:config', 'about:'], 13 | 'about:preferences': [hint_marker, 'about:preferences', 'about:'], 14 | 'https://www.bla.www.co.uk.fgehsu.kokoko.website.co.uk/en-US/products/firefox?as=u&utm_source=inproduct#asdfasdf=qwer': 15 | [ 16 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk/en-US/products/firefox', 17 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk/en-US/products', 18 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk/en-US', 19 | hint_marker, 20 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk', 21 | 'bla.www.co.uk.fgehsu.kokoko.website.co.uk', 22 | 'www.co.uk.fgehsu.kokoko.website.co.uk', 23 | 'co.uk.fgehsu.kokoko.website.co.uk', 24 | 'uk.fgehsu.kokoko.website.co.uk', 25 | 'fgehsu.kokoko.website.co.uk', 26 | 'kokoko.website.co.uk', 27 | 'website.co.uk', 28 | `${DUMB_DOMAIN_DETECTION}co.uk`, 29 | `${DUMB_DOMAIN_DETECTION}uk`, 30 | ], 31 | 'https://user:pass@www.bla.www.co.uk.fgehsu.kokoko.website.co.uk/en-US/products/firefox?as=u&utm_source=inproduct#asdfasdf=qwer': 32 | [ 33 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk/en-US/products/firefox', 34 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk/en-US/products', 35 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk/en-US', 36 | hint_marker, 37 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk', 38 | 'bla.www.co.uk.fgehsu.kokoko.website.co.uk', 39 | 'www.co.uk.fgehsu.kokoko.website.co.uk', 40 | 'co.uk.fgehsu.kokoko.website.co.uk', 41 | 'uk.fgehsu.kokoko.website.co.uk', 42 | 'fgehsu.kokoko.website.co.uk', 43 | 'kokoko.website.co.uk', 44 | 'website.co.uk', 45 | `${DUMB_DOMAIN_DETECTION}co.uk`, 46 | `${DUMB_DOMAIN_DETECTION}uk`, 47 | ], 48 | 'https://www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080/en-US/products/firefox?as=u&utm_source=inproduct#asdfasdf=qwer': 49 | [ 50 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080/en-US/products/firefox', 51 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080/en-US/products', 52 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080/en-US', 53 | hint_marker, 54 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080', 55 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk', 56 | 'bla.www.co.uk.fgehsu.kokoko.website.co.uk', 57 | 'www.co.uk.fgehsu.kokoko.website.co.uk', 58 | 'co.uk.fgehsu.kokoko.website.co.uk', 59 | 'uk.fgehsu.kokoko.website.co.uk', 60 | 'fgehsu.kokoko.website.co.uk', 61 | 'kokoko.website.co.uk', 62 | 'website.co.uk', 63 | `${DUMB_DOMAIN_DETECTION}co.uk`, 64 | `${DUMB_DOMAIN_DETECTION}uk`, 65 | ], 66 | 'https://user:pass@www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080/en-US/products/firefox?as=u&utm_source=inproduct#asdfasdf=qwer': 67 | [ 68 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080/en-US/products/firefox', 69 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080/en-US/products', 70 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080/en-US', 71 | hint_marker, 72 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk:8080', 73 | 'www.bla.www.co.uk.fgehsu.kokoko.website.co.uk', 74 | 'bla.www.co.uk.fgehsu.kokoko.website.co.uk', 75 | 'www.co.uk.fgehsu.kokoko.website.co.uk', 76 | 'co.uk.fgehsu.kokoko.website.co.uk', 77 | 'uk.fgehsu.kokoko.website.co.uk', 78 | 'fgehsu.kokoko.website.co.uk', 79 | 'kokoko.website.co.uk', 80 | 'website.co.uk', 81 | `${DUMB_DOMAIN_DETECTION}co.uk`, 82 | `${DUMB_DOMAIN_DETECTION}uk`, 83 | ], 84 | 'https://jfgeh.corp.company.local/en-US/products/firefox?as=u&utm_source=inproduct#asdfasdf=qwer': 85 | [ 86 | 'jfgeh.corp.company.local/en-US/products/firefox', 87 | 'jfgeh.corp.company.local/en-US/products', 88 | 'jfgeh.corp.company.local/en-US', 89 | hint_marker, 90 | 'jfgeh.corp.company.local', 91 | 'corp.company.local', 92 | 'company.local', 93 | `${DUMB_DOMAIN_DETECTION}local`, 94 | ], 95 | 'https://jfgeh/en-US/products/firefox?as=u&utm_source=inproduct#asdfasdf=qwer': 96 | [ 97 | 'jfgeh/en-US/products/firefox', 98 | 'jfgeh/en-US/products', 99 | 'jfgeh/en-US', 100 | hint_marker, 101 | 'jfgeh', 102 | ], 103 | 'https://example.com/test1234/hello': [ 104 | 'example.com/test1234/hello', 105 | 'example.com/test1234', 106 | hint_marker, 107 | 'example.com', 108 | `${DUMB_DOMAIN_DETECTION}com`, 109 | ], 110 | 'https://home.cern/science/physics/': [ 111 | 'home.cern/science/physics', 112 | 'home.cern/science', 113 | hint_marker, 114 | 'home.cern', 115 | `${DUMB_DOMAIN_DETECTION}cern`, 116 | ], 117 | 'https://support.mozilla.org/en-US/products/firefox?as=u&utm_source=inproduct#asdfasdf=qwer': 118 | [ 119 | 'support.mozilla.org/en-US/products/firefox', 120 | 'support.mozilla.org/en-US/products', 121 | 'support.mozilla.org/en-US', 122 | hint_marker, 123 | 'support.mozilla.org', 124 | 'mozilla.org', 125 | `${DUMB_DOMAIN_DETECTION}org`, 126 | ], 127 | 'https://support.mozilla.org/': [ 128 | hint_marker, 129 | 'support.mozilla.org', 130 | 'mozilla.org', 131 | `${DUMB_DOMAIN_DETECTION}org`, 132 | ], 133 | 'https://support.mozilla.org': [ 134 | hint_marker, 135 | 'support.mozilla.org', 136 | 'mozilla.org', 137 | `${DUMB_DOMAIN_DETECTION}org`, 138 | ], 139 | 'moz-extension://11256f55-552a-40ee-9c36-75a0be32137f/ui/preferences.html': [ 140 | 'moz-extension://11256f55-552a-40ee-9c36-75a0be32137f/ui/preferences.html', 141 | 'moz-extension://11256f55-552a-40ee-9c36-75a0be32137f/ui', 142 | hint_marker, 143 | 'moz-extension://11256f55-552a-40ee-9c36-75a0be32137f', 144 | 'moz-extension://', 145 | ], 146 | 'chrome-extension://mloybvmrdjplqsjvwgckcuevcfryxqqv/something/dashboard.html#settings.html': 147 | [ 148 | 'chrome-extension://mloybvmrdjplqsjvwgckcuevcfryxqqv/something/dashboard.html', 149 | 'chrome-extension://mloybvmrdjplqsjvwgckcuevcfryxqqv/something', 150 | hint_marker, 151 | 'chrome-extension://mloybvmrdjplqsjvwgckcuevcfryxqqv', 152 | 'chrome-extension://', 153 | ], 154 | 'file:///home/user/dark-background-light-text/test/test-pages/target-blank/1st.html': 155 | [ 156 | 'file:///home/user/dark-background-light-text/test/test-pages/target-blank/1st.html', 157 | 'file:///home/user/dark-background-light-text/test/test-pages/target-blank', 158 | 'file:///home/user/dark-background-light-text/test/test-pages', 159 | 'file:///home/user/dark-background-light-text/test', 160 | 'file:///home/user/dark-background-light-text', 161 | 'file:///home/user', 162 | hint_marker, 163 | 'file:///home', 164 | 'file://', 165 | ], 166 | 'file:///c:/Users/Public/Documents/dark-background-light-text/test/test-pages/target-blank/1st.html': 167 | [ 168 | 'file:///c:/Users/Public/Documents/dark-background-light-text/test/test-pages/target-blank/1st.html', 169 | 'file:///c:/Users/Public/Documents/dark-background-light-text/test/test-pages/target-blank', 170 | 'file:///c:/Users/Public/Documents/dark-background-light-text/test/test-pages', 171 | 'file:///c:/Users/Public/Documents/dark-background-light-text/test', 172 | 'file:///c:/Users/Public/Documents/dark-background-light-text', 173 | 'file:///c:/Users/Public/Documents', 174 | 'file:///c:/Users/Public', 175 | 'file:///c:/Users', 176 | hint_marker, 177 | 'file:///c:', 178 | 'file://', 179 | ], 180 | 'http://172.123.10.1/test/kokoko': [ 181 | '172.123.10.1/test/kokoko', 182 | '172.123.10.1/test', 183 | hint_marker, 184 | '172.123.10.1', 185 | ], 186 | 'http://user:pass@172.123.10.1/test/kokoko': [ 187 | '172.123.10.1/test/kokoko', 188 | '172.123.10.1/test', 189 | hint_marker, 190 | '172.123.10.1', 191 | ], 192 | 'http://172.123.10.1:8080/test/kokoko': [ 193 | '172.123.10.1:8080/test/kokoko', 194 | '172.123.10.1:8080/test', 195 | hint_marker, 196 | '172.123.10.1:8080', 197 | '172.123.10.1', 198 | ], 199 | 'http://user:pass@172.123.10.1:8080/test/kokoko': [ 200 | '172.123.10.1:8080/test/kokoko', 201 | '172.123.10.1:8080/test', 202 | hint_marker, 203 | '172.123.10.1:8080', 204 | '172.123.10.1', 205 | ], 206 | 'http://openwrt/cgi-bin/luci': [ 207 | 'openwrt/cgi-bin/luci', 208 | 'openwrt/cgi-bin', 209 | hint_marker, 210 | 'openwrt', 211 | ], 212 | 'http://openwrt:8080/cgi-bin/luci': [ 213 | 'openwrt:8080/cgi-bin/luci', 214 | 'openwrt:8080/cgi-bin', 215 | hint_marker, 216 | 'openwrt:8080', 217 | 'openwrt', 218 | ], 219 | 'http://[fdfe:5b0f:4148::1]/cgi-bin/luci': [ 220 | '[fdfe:5b0f:4148::1]/cgi-bin/luci', 221 | '[fdfe:5b0f:4148::1]/cgi-bin', 222 | hint_marker, 223 | '[fdfe:5b0f:4148::1]', 224 | ], 225 | 'http://user:pass@[fdfe:5b0f:4148::1]/cgi-bin/luci': [ 226 | '[fdfe:5b0f:4148::1]/cgi-bin/luci', 227 | '[fdfe:5b0f:4148::1]/cgi-bin', 228 | hint_marker, 229 | '[fdfe:5b0f:4148::1]', 230 | ], 231 | 'http://[fdfe:5b0f:4148::1]:8080/cgi-bin/luci': [ 232 | '[fdfe:5b0f:4148::1]:8080/cgi-bin/luci', 233 | '[fdfe:5b0f:4148::1]:8080/cgi-bin', 234 | hint_marker, 235 | '[fdfe:5b0f:4148::1]:8080', 236 | '[fdfe:5b0f:4148::1]', 237 | ], 238 | 'http://user:pass@[fdfe:5b0f:4148::1]:8080/cgi-bin/luci': [ 239 | '[fdfe:5b0f:4148::1]:8080/cgi-bin/luci', 240 | '[fdfe:5b0f:4148::1]:8080/cgi-bin', 241 | hint_marker, 242 | '[fdfe:5b0f:4148::1]:8080', 243 | '[fdfe:5b0f:4148::1]', 244 | ], 245 | 'https://xn--e1aybc.xn--p1acf/%D1%84%D1%8B%D0%B2%D0%B0/%D0%B9%D1%86%D1%83%D0%BA': 246 | [ 247 | 'xn--e1aybc.xn--p1acf/%D1%84%D1%8B%D0%B2%D0%B0/%D0%B9%D1%86%D1%83%D0%BA', 248 | 'xn--e1aybc.xn--p1acf/%D1%84%D1%8B%D0%B2%D0%B0', 249 | hint_marker, 250 | 'xn--e1aybc.xn--p1acf', 251 | `${DUMB_DOMAIN_DETECTION}xn--p1acf`, 252 | ], 253 | // broken 254 | // 'https://xn--e1aybc.xn--j1aef.xn--p1acf/%D1%84%D1%8B%D0%B2%D0%B0/%D0%B9%D1%86%D1%83%D0%BA': [ 255 | // 'xn--e1aybc.xn--j1aef.xn--p1acf/%D1%84%D1%8B%D0%B2%D0%B0/%D0%B9%D1%86%D1%83%D0%BA', 256 | // 'xn--e1aybc.xn--j1aef.xn--p1acf/%D1%84%D1%8B%D0%B2%D0%B0', 257 | // hint_marker, 258 | // 'xn--e1aybc.xn--j1aef.xn--p1acf', 259 | // `${DUMB_DOMAIN_DETECTION}xn--j1aef.xn--p1acf`, 260 | // `${DUMB_DOMAIN_DETECTION}xn--p1acf`, 261 | // ], 262 | }; 263 | 264 | const really_bad_urls = ['asdf']; 265 | 266 | function strip_dumb_marker(s: string): string { 267 | return s.replace(DUMB_DOMAIN_DETECTION, ''); 268 | } 269 | 270 | describe('generate_urls', () => { 271 | Object.entries(test_urls).forEach(([url, result]) => { 272 | it(`${url} — dumb domain detection`, () => { 273 | deepStrictEqual( 274 | generate_urls(url), 275 | result.filter((s) => s !== hint_marker).map(strip_dumb_marker), 276 | ); 277 | deepStrictEqual(generate_urls(url, true), result.map(strip_dumb_marker)); 278 | }); 279 | it(`${url} — smart domain detection`, () => { 280 | deepStrictEqual( 281 | smart_generate_urls(url), 282 | result.filter( 283 | (s) => s !== hint_marker && !s.startsWith(DUMB_DOMAIN_DETECTION), 284 | ), 285 | ); 286 | deepStrictEqual( 287 | smart_generate_urls(url, true), 288 | result.filter((s) => !s.startsWith(DUMB_DOMAIN_DETECTION)), 289 | ); 290 | }); 291 | }); 292 | really_bad_urls.forEach((url) => { 293 | it(url, () => { 294 | // in case if URL is really bad, original URL itself is returned 295 | // TODO: use mocking library? 296 | let console_error_called_times = 0; 297 | const orig = console.error; 298 | console.error = () => { 299 | console_error_called_times++; 300 | }; 301 | deepStrictEqual(generate_urls(url), [url]); 302 | assert(console_error_called_times === 1); 303 | deepStrictEqual(generate_urls(url, true), [hint_marker, url]); 304 | // TypeScript is not that smart 305 | assert((console_error_called_times as number) === 2); 306 | console.error = orig; 307 | }); 308 | }); 309 | }); 310 | 311 | const good_IPs = ['1.1.1.1', '12.34.56.78', '254.255.128.123', '0.0.0.0']; 312 | 313 | const bad_IPs = [ 314 | '1.1.1.1.1', 315 | '12.34.56.78.89', 316 | '254.255.128.123.122', 317 | '0.0.0.0.0', 318 | 319 | '1.1.1', 320 | '12.34.56', 321 | '254.255.128', 322 | '0.0.0', 323 | 324 | '1.1.1.1.', 325 | '12.34..56.78', 326 | '.254.255.128.123', 327 | 'a.0.0.0', 328 | 329 | '256.255.128.123', 330 | '252.2552.128.123', 331 | '252.2552.1.2', 332 | ]; 333 | 334 | describe('is_IPv4', () => { 335 | good_IPs.forEach((ip) => { 336 | it(`${ip} is IPv4`, () => assert(is_IPv4(ip) === true)); 337 | }); 338 | bad_IPs.forEach((ip) => { 339 | it(`${ip} is not IPv4`, () => assert(is_IPv4(ip) === false)); 340 | }); 341 | }); 342 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { parseCSSColor } from 'csscolorparser-ts'; 2 | import type { 3 | Runtime, 4 | ContentScripts, 5 | Manifest, 6 | ExtensionTypes, 7 | Storage, 8 | WebRequest, 9 | Tabs, 10 | Browser, 11 | } from 'webextension-polyfill'; 12 | import { 13 | ConfiguredPages, 14 | ConfiguredTabs, 15 | StylesheetRenderer, 16 | CallbackID, 17 | } from '../common/types'; 18 | import { 19 | get_prefs, 20 | set_pref, 21 | on_prefs_change, 22 | get_merged_configured_common, 23 | PrefsWithValues, 24 | } from '../common/shared'; 25 | import { methods } from '../methods/methods-with-stylesheets'; 26 | import { relative_luminance, strip_alpha } from '../common/color_utils'; 27 | import { modify_cors, modify_csp, version_lt } from './lib'; 28 | import * as base_style from '../methods/stylesheets/base'; 29 | 30 | declare const browser: Browser; 31 | 32 | const platform_info: Promise = 33 | 'getPlatformInfo' in browser.runtime 34 | ? browser.runtime.getPlatformInfo() 35 | : Promise.reject(); 36 | 37 | const configured_private: ConfiguredPages = {}; 38 | const configured_tabs: ConfiguredTabs = {}; 39 | function get_merged_configured(): Promise { 40 | return get_merged_configured_common(() => 41 | Promise.resolve(configured_private), 42 | ); 43 | } 44 | browser.tabs.onRemoved.addListener(async (tabId) => { 45 | try { 46 | if (Object.keys(configured_private).length > 0) { 47 | for (const tab of await browser.tabs.query({})) { 48 | if (tab.incognito) { 49 | return; 50 | } 51 | } 52 | for (const url of Object.keys(configured_private)) { 53 | delete configured_private[url]; 54 | } 55 | send_prefs({}); 56 | } 57 | if (Object.prototype.hasOwnProperty.call(configured_tabs, tabId)) { 58 | delete configured_tabs[tabId]; 59 | } 60 | } catch (e) { 61 | console.error(e); 62 | } 63 | }); 64 | 65 | function process_stylesheet( 66 | sheet: StylesheetRenderer, 67 | is_toplevel: boolean, 68 | // it's not very nice to make callers of the function query prefs themselves 69 | // however, this enables them to cache the value 70 | prefs: PrefsWithValues, 71 | ) { 72 | const is_darkbg = 73 | relative_luminance( 74 | strip_alpha(parseCSSColor(prefs.default_background_color as string)!), 75 | ) 76 | < relative_luminance( 77 | strip_alpha(parseCSSColor(prefs.default_foreground_color as string)!), 78 | ); 79 | return sheet.render({ 80 | default_foreground_color: prefs.default_foreground_color as string, 81 | default_background_color: prefs.default_background_color as string, 82 | default_link_color: prefs.default_link_color as string, 83 | default_visited_color: prefs.default_visited_color as string, 84 | default_active_color: prefs.default_active_color as string, 85 | default_selection_color: prefs.default_selection_color as string, 86 | is_toplevel, 87 | is_darkbg, 88 | }); 89 | } 90 | 91 | browser.runtime.onMessage.addListener(async (message, sender) => { 92 | try { 93 | // TODO: statically typed runtime.onMessage 94 | if (!Object.prototype.hasOwnProperty.call(message, 'action')) { 95 | console.error('bad message!', message); 96 | return; 97 | } 98 | switch (message.action) { 99 | case 'query_tabId': 100 | return sender.tab?.id; 101 | case CallbackID.INSERT_CSS: 102 | return await browser.tabs.insertCSS(sender.tab?.id, { 103 | code: message.code, 104 | frameId: sender.frameId, 105 | cssOrigin: 'user', 106 | runAt: 'document_start', 107 | }); 108 | case CallbackID.REMOVE_CSS: 109 | return await browser.tabs.removeCSS(sender.tab?.id, { 110 | code: message.code, 111 | frameId: sender.frameId, 112 | cssOrigin: 'user', 113 | runAt: 'document_start', 114 | }); 115 | case 'query_base_style': 116 | return process_stylesheet(base_style, true, await get_prefs()); 117 | case 'get_configured_private': 118 | return configured_private; 119 | case 'set_configured_private': 120 | if (message.value === null) { 121 | delete configured_private[message.key]; 122 | } else { 123 | configured_private[message.key] = message.value; 124 | } 125 | send_prefs({}); 126 | break; 127 | // @ts-ignore: 7029 128 | case 'get_my_tab_configuration': 129 | message.tab_id = sender.tab?.id; // eslint-disable-line no-param-reassign 130 | // falls through 131 | case 'get_tab_configuration': 132 | if ( 133 | Object.prototype.hasOwnProperty.call(configured_tabs, message.tab_id) 134 | ) { 135 | return configured_tabs[message.tab_id]; 136 | } 137 | return false; 138 | case 'set_configured_tab': 139 | if (message.value === null) { 140 | if ( 141 | Object.prototype.hasOwnProperty.call(configured_tabs, message.key) 142 | ) { 143 | delete configured_tabs[message.key]; 144 | } 145 | } else { 146 | configured_tabs[message.key] = message.value; 147 | } 148 | send_prefs({}); 149 | break; 150 | case 'open_options_page': 151 | // while runtime.openOptionsPage() works from browserAction page script, 152 | // due to bug 1414917 it behaves unintuitive on Fennec so here is a workaround 153 | if ((await platform_info).os === 'android') { 154 | setTimeout(() => browser.runtime.openOptionsPage(), 500); 155 | } else { 156 | browser.runtime.openOptionsPage(); 157 | } 158 | break; 159 | case 'is_commands_update_available': 160 | return ( 161 | Object.prototype.hasOwnProperty.call(browser, 'commands') 162 | && Object.prototype.hasOwnProperty.call(browser.commands, 'update') 163 | ); 164 | case 'query_parent_method_number': 165 | if (sender.frameId === 0) { 166 | console.error( 167 | 'Top-level frame requested some info about its parent. That should not happen. The sender is:', 168 | sender, 169 | ); 170 | return await get_prefs('default_method'); 171 | } 172 | return await browser.tabs.sendMessage( 173 | sender.tab!.id!, 174 | { action: 'get_method_number' }, 175 | { frameId: 0 }, 176 | ); 177 | default: 178 | console.error('bad message 2!', message); 179 | break; 180 | } 181 | } catch (e) { 182 | console.error(e); 183 | } 184 | }); 185 | 186 | const prev_scripts: ContentScripts.RegisteredContentScript[] = []; 187 | async function send_prefs(changes: { [s: string]: Storage.StorageChange }) { 188 | prev_scripts.forEach((cs) => cs.unregister()); 189 | prev_scripts.length = 0; 190 | const from_manifest = ( 191 | browser.runtime.getManifest() as Manifest.WebExtensionManifest 192 | ).content_scripts![0]; 193 | const new_data: ContentScripts.RegisteredContentScriptOptions = { 194 | matches: [''], 195 | }; 196 | const rendered_stylesheets: { [key: string]: string } = {}; 197 | const prefs = await get_prefs(); 198 | for (const css_renderer of new Set( 199 | Object.values(methods) 200 | .map((m) => m.stylesheets) 201 | .flat(), 202 | )) { 203 | rendered_stylesheets[`${css_renderer.name}_iframe`] = process_stylesheet( 204 | css_renderer, 205 | false, 206 | prefs, 207 | ); 208 | rendered_stylesheets[`${css_renderer.name}_toplevel`] = process_stylesheet( 209 | css_renderer, 210 | true, 211 | prefs, 212 | ); 213 | } 214 | const code = ` 215 | if (typeof content_script_state === 'undefined') { /* #226 part 1 workaround */ 216 | window.content_script_state = 'registered_content_script_first'; 217 | } 218 | 219 | window.prefs = ${JSON.stringify(await get_prefs())}; 220 | window.merged_configured = ${JSON.stringify( 221 | await get_merged_configured(), 222 | )}; 223 | window.configured_tabs = ${JSON.stringify(configured_tabs)}; 224 | window.rendered_stylesheets = ${JSON.stringify(rendered_stylesheets)}; 225 | if (window.content_script_state !== 'registered_content_script_first') { /* #226 part 1 workaround */ 226 | window.do_it(${JSON.stringify(changes)}); 227 | } 228 | `; 229 | for (const key of Object.keys(from_manifest)) { 230 | if (key === 'js') { 231 | new_data.js = [{ code }]; 232 | } else { 233 | // convert to camelCase 234 | const new_key = key 235 | .split('_') 236 | .map((el, index) => 237 | index === 0 ? el : el.charAt(0).toUpperCase() + el.slice(1), 238 | ) 239 | .join(''); 240 | (new_data as any)[new_key] = (from_manifest as any)[key]; 241 | } 242 | } 243 | prev_scripts.push(await browser.contentScripts.register(new_data)); 244 | 245 | // same for already loaded pages 246 | const new_data_for_tabs: ExtensionTypes.InjectDetails = { code }; 247 | for (const key of Object.keys(new_data)) { 248 | if (['allFrames', 'matchAboutBlank', 'runAt'].indexOf(key) >= 0) { 249 | (new_data_for_tabs as any)[key] = (new_data as any)[key]; 250 | } 251 | } 252 | for (const tab of await browser.tabs.query({})) { 253 | browser.tabs.executeScript(tab.id, new_data_for_tabs); 254 | } 255 | } 256 | send_prefs({}); 257 | on_prefs_change(send_prefs); 258 | 259 | if (Object.prototype.hasOwnProperty.call(browser, 'commands')) { 260 | browser.commands.onCommand.addListener(async (name) => { 261 | try { 262 | let current_tab: Tabs.Tab; 263 | switch (name) { 264 | case 'global_toggle_hotkey': 265 | set_pref('enabled', !(await get_prefs('enabled'))); 266 | break; 267 | case 'tab_toggle_hotkey': 268 | [current_tab] = await browser.tabs.query({ 269 | currentWindow: true, 270 | active: true, 271 | }); 272 | if ( 273 | Object.prototype.hasOwnProperty.call( 274 | configured_tabs, 275 | current_tab.id!, 276 | ) 277 | ) { 278 | delete configured_tabs[current_tab.id!]; 279 | } else { 280 | configured_tabs[current_tab.id!] = '0'; 281 | } 282 | send_prefs({}); 283 | break; 284 | default: 285 | console.error('bad command'); 286 | break; 287 | } 288 | } catch (e) { 289 | console.error(e); 290 | } 291 | }); 292 | } 293 | 294 | get_prefs('do_not_set_overrideDocumentColors_to_never').then((val) => { 295 | if (!val) { 296 | // The extension can barely do anything when overrideDocumentColors == always 297 | // or overrideDocumentColors == high-contrast-only is set and high contrast mode is in use 298 | browser.browserSettings.overrideDocumentColors 299 | .set({ value: 'never' }) 300 | .catch((error) => console.error(error)); 301 | } 302 | }); 303 | 304 | browser.runtime.onInstalled.addListener((details) => { 305 | if ( 306 | details.reason === 'install' 307 | || (details.reason === 'update' 308 | && details.previousVersion 309 | && version_lt(details.previousVersion, '0.7.6')) 310 | ) { 311 | browser.webRequest.handlerBehaviorChanged(); 312 | } 313 | }); 314 | 315 | browser.webRequest.onHeadersReceived.addListener( 316 | (details) => { 317 | try { 318 | return { 319 | responseHeaders: details.responseHeaders!.map(modify_csp), 320 | }; 321 | } catch (e) { 322 | console.error(e); 323 | return {}; 324 | } 325 | }, 326 | { 327 | urls: [''], 328 | types: ['main_frame'], 329 | }, 330 | ['blocking', 'responseHeaders'], 331 | ); 332 | 333 | function is_probably_service_worker( 334 | details: WebRequest.OnHeadersReceivedDetailsType, 335 | ): boolean { 336 | if (!details.originUrl) { 337 | return false; 338 | } 339 | const origin_url = new URL(details.originUrl); 340 | // likely a request from Service Worker 341 | if ( 342 | details.type === 'xmlhttprequest' 343 | && details.tabId === -1 344 | && (origin_url.protocol === 'https:' 345 | || origin_url.hostname === 'localhost' 346 | || origin_url.hostname === '127.0.0.1' 347 | || origin_url.hostname === '[::1]') 348 | ) { 349 | return true; 350 | } 351 | return false; 352 | } 353 | 354 | function get_content_type( 355 | headers?: WebRequest.HttpHeaders, 356 | ): string | undefined { 357 | return headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value; 358 | } 359 | 360 | browser.webRequest.onHeadersReceived.addListener( 361 | (details) => { 362 | if ( 363 | details.type === 'stylesheet' 364 | || (is_probably_service_worker(details) 365 | && get_content_type(details.responseHeaders)?.startsWith('text/css')) 366 | ) { 367 | try { 368 | return { 369 | responseHeaders: modify_cors(details.responseHeaders!, details), 370 | }; 371 | } catch (e) { 372 | console.error(e); 373 | return {}; 374 | } 375 | } 376 | return {}; 377 | }, 378 | { 379 | urls: [''], 380 | types: ['stylesheet', 'xmlhttprequest'], 381 | }, 382 | ['blocking', 'responseHeaders'], 383 | ); 384 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /ui/bootstrap.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.0.0-alpha.3 (http://getbootstrap.com) 3 | * Copyright 2011-2016 The Bootstrap Authors 4 | * Copyright 2011-2016 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | */ 7 | .container { 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding-left: 15px; 11 | padding-right: 15px; 12 | } 13 | 14 | .container::after { 15 | content: ''; 16 | display: table; 17 | clear: both; 18 | } 19 | 20 | @media (min-width: 544px) { 21 | .container { 22 | max-width: 576px; 23 | } 24 | } 25 | 26 | @media (min-width: 768px) { 27 | .container { 28 | max-width: 720px; 29 | } 30 | } 31 | 32 | @media (min-width: 992px) { 33 | .container { 34 | max-width: 940px; 35 | } 36 | } 37 | 38 | @media (min-width: 1200px) { 39 | .container { 40 | max-width: 1140px; 41 | } 42 | } 43 | 44 | .container-fluid { 45 | margin-left: auto; 46 | margin-right: auto; 47 | padding-left: 15px; 48 | padding-right: 15px; 49 | } 50 | 51 | .container-fluid::after { 52 | content: ''; 53 | display: table; 54 | clear: both; 55 | } 56 | 57 | .row { 58 | margin-left: -15px; 59 | margin-right: -15px; 60 | } 61 | 62 | .row::after { 63 | content: ''; 64 | display: table; 65 | clear: both; 66 | } 67 | 68 | .col-xs-1, 69 | .col-xs-2, 70 | .col-xs-3, 71 | .col-xs-4, 72 | .col-xs-5, 73 | .col-xs-6, 74 | .col-xs-7, 75 | .col-xs-8, 76 | .col-xs-9, 77 | .col-xs-10, 78 | .col-xs-11, 79 | .col-xs-12, 80 | .col-sm-1, 81 | .col-sm-2, 82 | .col-sm-3, 83 | .col-sm-4, 84 | .col-sm-5, 85 | .col-sm-6, 86 | .col-sm-7, 87 | .col-sm-8, 88 | .col-sm-9, 89 | .col-sm-10, 90 | .col-sm-11, 91 | .col-sm-12, 92 | .col-md-1, 93 | .col-md-2, 94 | .col-md-3, 95 | .col-md-4, 96 | .col-md-5, 97 | .col-md-6, 98 | .col-md-7, 99 | .col-md-8, 100 | .col-md-9, 101 | .col-md-10, 102 | .col-md-11, 103 | .col-md-12, 104 | .col-lg-1, 105 | .col-lg-2, 106 | .col-lg-3, 107 | .col-lg-4, 108 | .col-lg-5, 109 | .col-lg-6, 110 | .col-lg-7, 111 | .col-lg-8, 112 | .col-lg-9, 113 | .col-lg-10, 114 | .col-lg-11, 115 | .col-lg-12, 116 | .col-xl-1, 117 | .col-xl-2, 118 | .col-xl-3, 119 | .col-xl-4, 120 | .col-xl-5, 121 | .col-xl-6, 122 | .col-xl-7, 123 | .col-xl-8, 124 | .col-xl-9, 125 | .col-xl-10, 126 | .col-xl-11, 127 | .col-xl-12 { 128 | position: relative; 129 | min-height: 1px; 130 | padding-right: 15px; 131 | padding-left: 15px; 132 | } 133 | 134 | .col-xs-1 { 135 | float: left; 136 | width: 8.333333%; 137 | } 138 | 139 | .col-xs-2 { 140 | float: left; 141 | width: 16.666667%; 142 | } 143 | 144 | .col-xs-3 { 145 | float: left; 146 | width: 25%; 147 | } 148 | 149 | .col-xs-4 { 150 | float: left; 151 | width: 33.333333%; 152 | } 153 | 154 | .col-xs-5 { 155 | float: left; 156 | width: 41.666667%; 157 | } 158 | 159 | .col-xs-6 { 160 | float: left; 161 | width: 50%; 162 | } 163 | 164 | .col-xs-7 { 165 | float: left; 166 | width: 58.333333%; 167 | } 168 | 169 | .col-xs-8 { 170 | float: left; 171 | width: 66.666667%; 172 | } 173 | 174 | .col-xs-9 { 175 | float: left; 176 | width: 75%; 177 | } 178 | 179 | .col-xs-10 { 180 | float: left; 181 | width: 83.333333%; 182 | } 183 | 184 | .col-xs-11 { 185 | float: left; 186 | width: 91.666667%; 187 | } 188 | 189 | .col-xs-12 { 190 | float: left; 191 | width: 100%; 192 | } 193 | 194 | .pull-xs-0 { 195 | right: auto; 196 | } 197 | 198 | .pull-xs-1 { 199 | right: 8.333333%; 200 | } 201 | 202 | .pull-xs-2 { 203 | right: 16.666667%; 204 | } 205 | 206 | .pull-xs-3 { 207 | right: 25%; 208 | } 209 | 210 | .pull-xs-4 { 211 | right: 33.333333%; 212 | } 213 | 214 | .pull-xs-5 { 215 | right: 41.666667%; 216 | } 217 | 218 | .pull-xs-6 { 219 | right: 50%; 220 | } 221 | 222 | .pull-xs-7 { 223 | right: 58.333333%; 224 | } 225 | 226 | .pull-xs-8 { 227 | right: 66.666667%; 228 | } 229 | 230 | .pull-xs-9 { 231 | right: 75%; 232 | } 233 | 234 | .pull-xs-10 { 235 | right: 83.333333%; 236 | } 237 | 238 | .pull-xs-11 { 239 | right: 91.666667%; 240 | } 241 | 242 | .pull-xs-12 { 243 | right: 100%; 244 | } 245 | 246 | .push-xs-0 { 247 | left: auto; 248 | } 249 | 250 | .push-xs-1 { 251 | left: 8.333333%; 252 | } 253 | 254 | .push-xs-2 { 255 | left: 16.666667%; 256 | } 257 | 258 | .push-xs-3 { 259 | left: 25%; 260 | } 261 | 262 | .push-xs-4 { 263 | left: 33.333333%; 264 | } 265 | 266 | .push-xs-5 { 267 | left: 41.666667%; 268 | } 269 | 270 | .push-xs-6 { 271 | left: 50%; 272 | } 273 | 274 | .push-xs-7 { 275 | left: 58.333333%; 276 | } 277 | 278 | .push-xs-8 { 279 | left: 66.666667%; 280 | } 281 | 282 | .push-xs-9 { 283 | left: 75%; 284 | } 285 | 286 | .push-xs-10 { 287 | left: 83.333333%; 288 | } 289 | 290 | .push-xs-11 { 291 | left: 91.666667%; 292 | } 293 | 294 | .push-xs-12 { 295 | left: 100%; 296 | } 297 | 298 | .offset-xs-1 { 299 | margin-left: 8.333333%; 300 | } 301 | 302 | .offset-xs-2 { 303 | margin-left: 16.666667%; 304 | } 305 | 306 | .offset-xs-3 { 307 | margin-left: 25%; 308 | } 309 | 310 | .offset-xs-4 { 311 | margin-left: 33.333333%; 312 | } 313 | 314 | .offset-xs-5 { 315 | margin-left: 41.666667%; 316 | } 317 | 318 | .offset-xs-6 { 319 | margin-left: 50%; 320 | } 321 | 322 | .offset-xs-7 { 323 | margin-left: 58.333333%; 324 | } 325 | 326 | .offset-xs-8 { 327 | margin-left: 66.666667%; 328 | } 329 | 330 | .offset-xs-9 { 331 | margin-left: 75%; 332 | } 333 | 334 | .offset-xs-10 { 335 | margin-left: 83.333333%; 336 | } 337 | 338 | .offset-xs-11 { 339 | margin-left: 91.666667%; 340 | } 341 | 342 | @media (min-width: 544px) { 343 | .col-sm-1 { 344 | float: left; 345 | width: 8.333333%; 346 | } 347 | .col-sm-2 { 348 | float: left; 349 | width: 16.666667%; 350 | } 351 | .col-sm-3 { 352 | float: left; 353 | width: 25%; 354 | } 355 | .col-sm-4 { 356 | float: left; 357 | width: 33.333333%; 358 | } 359 | .col-sm-5 { 360 | float: left; 361 | width: 41.666667%; 362 | } 363 | .col-sm-6 { 364 | float: left; 365 | width: 50%; 366 | } 367 | .col-sm-7 { 368 | float: left; 369 | width: 58.333333%; 370 | } 371 | .col-sm-8 { 372 | float: left; 373 | width: 66.666667%; 374 | } 375 | .col-sm-9 { 376 | float: left; 377 | width: 75%; 378 | } 379 | .col-sm-10 { 380 | float: left; 381 | width: 83.333333%; 382 | } 383 | .col-sm-11 { 384 | float: left; 385 | width: 91.666667%; 386 | } 387 | .col-sm-12 { 388 | float: left; 389 | width: 100%; 390 | } 391 | .pull-sm-0 { 392 | right: auto; 393 | } 394 | .pull-sm-1 { 395 | right: 8.333333%; 396 | } 397 | .pull-sm-2 { 398 | right: 16.666667%; 399 | } 400 | .pull-sm-3 { 401 | right: 25%; 402 | } 403 | .pull-sm-4 { 404 | right: 33.333333%; 405 | } 406 | .pull-sm-5 { 407 | right: 41.666667%; 408 | } 409 | .pull-sm-6 { 410 | right: 50%; 411 | } 412 | .pull-sm-7 { 413 | right: 58.333333%; 414 | } 415 | .pull-sm-8 { 416 | right: 66.666667%; 417 | } 418 | .pull-sm-9 { 419 | right: 75%; 420 | } 421 | .pull-sm-10 { 422 | right: 83.333333%; 423 | } 424 | .pull-sm-11 { 425 | right: 91.666667%; 426 | } 427 | .pull-sm-12 { 428 | right: 100%; 429 | } 430 | .push-sm-0 { 431 | left: auto; 432 | } 433 | .push-sm-1 { 434 | left: 8.333333%; 435 | } 436 | .push-sm-2 { 437 | left: 16.666667%; 438 | } 439 | .push-sm-3 { 440 | left: 25%; 441 | } 442 | .push-sm-4 { 443 | left: 33.333333%; 444 | } 445 | .push-sm-5 { 446 | left: 41.666667%; 447 | } 448 | .push-sm-6 { 449 | left: 50%; 450 | } 451 | .push-sm-7 { 452 | left: 58.333333%; 453 | } 454 | .push-sm-8 { 455 | left: 66.666667%; 456 | } 457 | .push-sm-9 { 458 | left: 75%; 459 | } 460 | .push-sm-10 { 461 | left: 83.333333%; 462 | } 463 | .push-sm-11 { 464 | left: 91.666667%; 465 | } 466 | .push-sm-12 { 467 | left: 100%; 468 | } 469 | .offset-sm-0 { 470 | margin-left: 0%; 471 | } 472 | .offset-sm-1 { 473 | margin-left: 8.333333%; 474 | } 475 | .offset-sm-2 { 476 | margin-left: 16.666667%; 477 | } 478 | .offset-sm-3 { 479 | margin-left: 25%; 480 | } 481 | .offset-sm-4 { 482 | margin-left: 33.333333%; 483 | } 484 | .offset-sm-5 { 485 | margin-left: 41.666667%; 486 | } 487 | .offset-sm-6 { 488 | margin-left: 50%; 489 | } 490 | .offset-sm-7 { 491 | margin-left: 58.333333%; 492 | } 493 | .offset-sm-8 { 494 | margin-left: 66.666667%; 495 | } 496 | .offset-sm-9 { 497 | margin-left: 75%; 498 | } 499 | .offset-sm-10 { 500 | margin-left: 83.333333%; 501 | } 502 | .offset-sm-11 { 503 | margin-left: 91.666667%; 504 | } 505 | } 506 | 507 | @media (min-width: 768px) { 508 | .col-md-1 { 509 | float: left; 510 | width: 8.333333%; 511 | } 512 | .col-md-2 { 513 | float: left; 514 | width: 16.666667%; 515 | } 516 | .col-md-3 { 517 | float: left; 518 | width: 25%; 519 | } 520 | .col-md-4 { 521 | float: left; 522 | width: 33.333333%; 523 | } 524 | .col-md-5 { 525 | float: left; 526 | width: 41.666667%; 527 | } 528 | .col-md-6 { 529 | float: left; 530 | width: 50%; 531 | } 532 | .col-md-7 { 533 | float: left; 534 | width: 58.333333%; 535 | } 536 | .col-md-8 { 537 | float: left; 538 | width: 66.666667%; 539 | } 540 | .col-md-9 { 541 | float: left; 542 | width: 75%; 543 | } 544 | .col-md-10 { 545 | float: left; 546 | width: 83.333333%; 547 | } 548 | .col-md-11 { 549 | float: left; 550 | width: 91.666667%; 551 | } 552 | .col-md-12 { 553 | float: left; 554 | width: 100%; 555 | } 556 | .pull-md-0 { 557 | right: auto; 558 | } 559 | .pull-md-1 { 560 | right: 8.333333%; 561 | } 562 | .pull-md-2 { 563 | right: 16.666667%; 564 | } 565 | .pull-md-3 { 566 | right: 25%; 567 | } 568 | .pull-md-4 { 569 | right: 33.333333%; 570 | } 571 | .pull-md-5 { 572 | right: 41.666667%; 573 | } 574 | .pull-md-6 { 575 | right: 50%; 576 | } 577 | .pull-md-7 { 578 | right: 58.333333%; 579 | } 580 | .pull-md-8 { 581 | right: 66.666667%; 582 | } 583 | .pull-md-9 { 584 | right: 75%; 585 | } 586 | .pull-md-10 { 587 | right: 83.333333%; 588 | } 589 | .pull-md-11 { 590 | right: 91.666667%; 591 | } 592 | .pull-md-12 { 593 | right: 100%; 594 | } 595 | .push-md-0 { 596 | left: auto; 597 | } 598 | .push-md-1 { 599 | left: 8.333333%; 600 | } 601 | .push-md-2 { 602 | left: 16.666667%; 603 | } 604 | .push-md-3 { 605 | left: 25%; 606 | } 607 | .push-md-4 { 608 | left: 33.333333%; 609 | } 610 | .push-md-5 { 611 | left: 41.666667%; 612 | } 613 | .push-md-6 { 614 | left: 50%; 615 | } 616 | .push-md-7 { 617 | left: 58.333333%; 618 | } 619 | .push-md-8 { 620 | left: 66.666667%; 621 | } 622 | .push-md-9 { 623 | left: 75%; 624 | } 625 | .push-md-10 { 626 | left: 83.333333%; 627 | } 628 | .push-md-11 { 629 | left: 91.666667%; 630 | } 631 | .push-md-12 { 632 | left: 100%; 633 | } 634 | .offset-md-0 { 635 | margin-left: 0%; 636 | } 637 | .offset-md-1 { 638 | margin-left: 8.333333%; 639 | } 640 | .offset-md-2 { 641 | margin-left: 16.666667%; 642 | } 643 | .offset-md-3 { 644 | margin-left: 25%; 645 | } 646 | .offset-md-4 { 647 | margin-left: 33.333333%; 648 | } 649 | .offset-md-5 { 650 | margin-left: 41.666667%; 651 | } 652 | .offset-md-6 { 653 | margin-left: 50%; 654 | } 655 | .offset-md-7 { 656 | margin-left: 58.333333%; 657 | } 658 | .offset-md-8 { 659 | margin-left: 66.666667%; 660 | } 661 | .offset-md-9 { 662 | margin-left: 75%; 663 | } 664 | .offset-md-10 { 665 | margin-left: 83.333333%; 666 | } 667 | .offset-md-11 { 668 | margin-left: 91.666667%; 669 | } 670 | } 671 | 672 | @media (min-width: 992px) { 673 | .col-lg-1 { 674 | float: left; 675 | width: 8.333333%; 676 | } 677 | .col-lg-2 { 678 | float: left; 679 | width: 16.666667%; 680 | } 681 | .col-lg-3 { 682 | float: left; 683 | width: 25%; 684 | } 685 | .col-lg-4 { 686 | float: left; 687 | width: 33.333333%; 688 | } 689 | .col-lg-5 { 690 | float: left; 691 | width: 41.666667%; 692 | } 693 | .col-lg-6 { 694 | float: left; 695 | width: 50%; 696 | } 697 | .col-lg-7 { 698 | float: left; 699 | width: 58.333333%; 700 | } 701 | .col-lg-8 { 702 | float: left; 703 | width: 66.666667%; 704 | } 705 | .col-lg-9 { 706 | float: left; 707 | width: 75%; 708 | } 709 | .col-lg-10 { 710 | float: left; 711 | width: 83.333333%; 712 | } 713 | .col-lg-11 { 714 | float: left; 715 | width: 91.666667%; 716 | } 717 | .col-lg-12 { 718 | float: left; 719 | width: 100%; 720 | } 721 | .pull-lg-0 { 722 | right: auto; 723 | } 724 | .pull-lg-1 { 725 | right: 8.333333%; 726 | } 727 | .pull-lg-2 { 728 | right: 16.666667%; 729 | } 730 | .pull-lg-3 { 731 | right: 25%; 732 | } 733 | .pull-lg-4 { 734 | right: 33.333333%; 735 | } 736 | .pull-lg-5 { 737 | right: 41.666667%; 738 | } 739 | .pull-lg-6 { 740 | right: 50%; 741 | } 742 | .pull-lg-7 { 743 | right: 58.333333%; 744 | } 745 | .pull-lg-8 { 746 | right: 66.666667%; 747 | } 748 | .pull-lg-9 { 749 | right: 75%; 750 | } 751 | .pull-lg-10 { 752 | right: 83.333333%; 753 | } 754 | .pull-lg-11 { 755 | right: 91.666667%; 756 | } 757 | .pull-lg-12 { 758 | right: 100%; 759 | } 760 | .push-lg-0 { 761 | left: auto; 762 | } 763 | .push-lg-1 { 764 | left: 8.333333%; 765 | } 766 | .push-lg-2 { 767 | left: 16.666667%; 768 | } 769 | .push-lg-3 { 770 | left: 25%; 771 | } 772 | .push-lg-4 { 773 | left: 33.333333%; 774 | } 775 | .push-lg-5 { 776 | left: 41.666667%; 777 | } 778 | .push-lg-6 { 779 | left: 50%; 780 | } 781 | .push-lg-7 { 782 | left: 58.333333%; 783 | } 784 | .push-lg-8 { 785 | left: 66.666667%; 786 | } 787 | .push-lg-9 { 788 | left: 75%; 789 | } 790 | .push-lg-10 { 791 | left: 83.333333%; 792 | } 793 | .push-lg-11 { 794 | left: 91.666667%; 795 | } 796 | .push-lg-12 { 797 | left: 100%; 798 | } 799 | .offset-lg-0 { 800 | margin-left: 0%; 801 | } 802 | .offset-lg-1 { 803 | margin-left: 8.333333%; 804 | } 805 | .offset-lg-2 { 806 | margin-left: 16.666667%; 807 | } 808 | .offset-lg-3 { 809 | margin-left: 25%; 810 | } 811 | .offset-lg-4 { 812 | margin-left: 33.333333%; 813 | } 814 | .offset-lg-5 { 815 | margin-left: 41.666667%; 816 | } 817 | .offset-lg-6 { 818 | margin-left: 50%; 819 | } 820 | .offset-lg-7 { 821 | margin-left: 58.333333%; 822 | } 823 | .offset-lg-8 { 824 | margin-left: 66.666667%; 825 | } 826 | .offset-lg-9 { 827 | margin-left: 75%; 828 | } 829 | .offset-lg-10 { 830 | margin-left: 83.333333%; 831 | } 832 | .offset-lg-11 { 833 | margin-left: 91.666667%; 834 | } 835 | } 836 | 837 | @media (min-width: 1200px) { 838 | .col-xl-1 { 839 | float: left; 840 | width: 8.333333%; 841 | } 842 | .col-xl-2 { 843 | float: left; 844 | width: 16.666667%; 845 | } 846 | .col-xl-3 { 847 | float: left; 848 | width: 25%; 849 | } 850 | .col-xl-4 { 851 | float: left; 852 | width: 33.333333%; 853 | } 854 | .col-xl-5 { 855 | float: left; 856 | width: 41.666667%; 857 | } 858 | .col-xl-6 { 859 | float: left; 860 | width: 50%; 861 | } 862 | .col-xl-7 { 863 | float: left; 864 | width: 58.333333%; 865 | } 866 | .col-xl-8 { 867 | float: left; 868 | width: 66.666667%; 869 | } 870 | .col-xl-9 { 871 | float: left; 872 | width: 75%; 873 | } 874 | .col-xl-10 { 875 | float: left; 876 | width: 83.333333%; 877 | } 878 | .col-xl-11 { 879 | float: left; 880 | width: 91.666667%; 881 | } 882 | .col-xl-12 { 883 | float: left; 884 | width: 100%; 885 | } 886 | .pull-xl-0 { 887 | right: auto; 888 | } 889 | .pull-xl-1 { 890 | right: 8.333333%; 891 | } 892 | .pull-xl-2 { 893 | right: 16.666667%; 894 | } 895 | .pull-xl-3 { 896 | right: 25%; 897 | } 898 | .pull-xl-4 { 899 | right: 33.333333%; 900 | } 901 | .pull-xl-5 { 902 | right: 41.666667%; 903 | } 904 | .pull-xl-6 { 905 | right: 50%; 906 | } 907 | .pull-xl-7 { 908 | right: 58.333333%; 909 | } 910 | .pull-xl-8 { 911 | right: 66.666667%; 912 | } 913 | .pull-xl-9 { 914 | right: 75%; 915 | } 916 | .pull-xl-10 { 917 | right: 83.333333%; 918 | } 919 | .pull-xl-11 { 920 | right: 91.666667%; 921 | } 922 | .pull-xl-12 { 923 | right: 100%; 924 | } 925 | .push-xl-0 { 926 | left: auto; 927 | } 928 | .push-xl-1 { 929 | left: 8.333333%; 930 | } 931 | .push-xl-2 { 932 | left: 16.666667%; 933 | } 934 | .push-xl-3 { 935 | left: 25%; 936 | } 937 | .push-xl-4 { 938 | left: 33.333333%; 939 | } 940 | .push-xl-5 { 941 | left: 41.666667%; 942 | } 943 | .push-xl-6 { 944 | left: 50%; 945 | } 946 | .push-xl-7 { 947 | left: 58.333333%; 948 | } 949 | .push-xl-8 { 950 | left: 66.666667%; 951 | } 952 | .push-xl-9 { 953 | left: 75%; 954 | } 955 | .push-xl-10 { 956 | left: 83.333333%; 957 | } 958 | .push-xl-11 { 959 | left: 91.666667%; 960 | } 961 | .push-xl-12 { 962 | left: 100%; 963 | } 964 | .offset-xl-0 { 965 | margin-left: 0%; 966 | } 967 | .offset-xl-1 { 968 | margin-left: 8.333333%; 969 | } 970 | .offset-xl-2 { 971 | margin-left: 16.666667%; 972 | } 973 | .offset-xl-3 { 974 | margin-left: 25%; 975 | } 976 | .offset-xl-4 { 977 | margin-left: 33.333333%; 978 | } 979 | .offset-xl-5 { 980 | margin-left: 41.666667%; 981 | } 982 | .offset-xl-6 { 983 | margin-left: 50%; 984 | } 985 | .offset-xl-7 { 986 | margin-left: 58.333333%; 987 | } 988 | .offset-xl-8 { 989 | margin-left: 66.666667%; 990 | } 991 | .offset-xl-9 { 992 | margin-left: 75%; 993 | } 994 | .offset-xl-10 { 995 | margin-left: 83.333333%; 996 | } 997 | .offset-xl-11 { 998 | margin-left: 91.666667%; 999 | } 1000 | } 1001 | /*# sourceMappingURL=bootstrap.css.map */ 1002 | --------------------------------------------------------------------------------