",
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 |
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 | recreate element
21 | refill style
22 |
23 |
37 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/test/test-pages/invert-test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Invert test
6 |
39 |
40 |
41 | Toggle filter
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 |
40 | {#each current_preferences as pref (pref.name)}
41 | {#if pref.type !== 'configured_pages'}
42 |
43 |
44 | {pref.title}
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 |
63 | set_pref(pref.name, e.currentTarget.selectedIndex)}
64 | class="pref_{pref.name} full-width form-control"
65 | id="labeled_pref_{pref.name}"
66 | data-pref-type={pref.type}
67 | >
68 | {#each pref.options as option (option.value)}
69 | {option.label}
72 | {/each}
73 |
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 |
86 | set_pref(pref.name, prefs_keys_with_defaults[pref.name])}
87 | class="btn btn-default full-width">Reset
89 |
90 |
91 | {/if}
92 | {/each}
93 |
94 | {#await browser.runtime.getPlatformInfo() then platformInfo}
95 | {#if platformInfo.os !== 'android'}
96 |
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 |
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 |
121 | set_pref(
122 | 'configured_pages',
123 | Object.fromEntries(
124 | Object.entries(configured_pages).filter(([k, _]) => k !== page),
125 | ),
126 | )}
127 | class="btn btn-default full-width">Remove
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 | {/each}
261 |
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 |
--------------------------------------------------------------------------------