From d054dcf42e82f22eebe028c18d2c405d71949f56 Mon Sep 17 00:00:00 2001
From: tophf
Date: Mon, 30 Nov 2020 03:29:35 +0300
Subject: [PATCH] migrate to AMD modules
---
.eslintignore | 4 +-
.eslintrc.yml | 24 +-
README.md | 6 +-
background/background-api.js | 154 ++
background/background-worker.js | 172 +-
background/background.js | 203 +-
background/browser-cmd-hotkeys.js | 23 +
background/content-scripts.js | 23 +-
background/context-menus.js | 31 +-
background/db-chrome-storage.js | 89 +-
background/db.js | 34 +-
background/icon-manager.js | 69 +-
background/icon-util.js | 140 +-
background/navigator-util.js | 56 +-
background/openusercss-api.js | 12 +-
background/remove-unused-storage.js | 25 +
background/search-db.js | 127 +-
background/style-manager.js | 105 +-
background/style-via-api.js | 45 +-
background/style-via-webrequest.js | 29 +-
background/sync.js | 153 +-
background/tab-manager.js | 45 +-
background/token-manager.js | 322 +--
background/update.js | 343 ++--
background/usercss-api-helper.js | 222 +-
background/usercss-install-helper.js | 120 +-
content/apply.js | 16 +-
content/install-hook-greasyfork.js | 31 +-
content/install-hook-openusercss.js | 11 +-
content/install-hook-userstyles.js | 10 +-
content/style-injector.js | 35 +-
edit.html | 240 +--
edit/autocomplete.js | 198 ++
edit/beautify.js | 96 +-
edit/codemirror-default.css | 3 -
edit/codemirror-default.js | 106 +-
edit/codemirror-factory.js | 308 +--
edit/codemirror-themes.js | 5 +-
edit/colorpicker-helper.js | 35 +-
edit/edit.js | 878 +++-----
edit/editor-worker.js | 163 +-
edit/editor.js | 199 ++
edit/global-search.js | 54 +-
edit/{show-keymap-help.js => keymap-help.js} | 74 +-
edit/linter-config-dialog.js | 197 --
edit/linter-defaults.js | 222 --
edit/linter-dialogs.js | 238 +++
edit/linter-engines.js | 115 --
edit/linter-help-dialog.js | 52 -
edit/linter-manager.js | 252 +++
edit/linter-meta.js | 44 -
edit/linter-report.js | 54 +-
edit/linter.js | 77 -
edit/live-preview.js | 133 +-
edit/moz-section-finder.js | 36 +-
edit/moz-section-widget.js | 60 +-
edit/preinit.js | 82 +
edit/regexp-tester.js | 164 +-
edit/reroute-hotkeys.js | 41 +-
edit/sections-editor-section.js | 767 ++++---
edit/sections-editor.js | 90 +-
edit/source-editor.js | 129 +-
edit/util.js | 422 ++--
install-usercss.html | 37 +-
install-usercss/install-usercss.css | 4 +
install-usercss/install-usercss.js | 516 +++--
install-usercss/preinit.js | 90 +
js/cache.js | 131 +-
.../colorpicker => js/color}/LICENSE | 0
.../colorpicker => js/color}/README.md | 0
js/color/color-converter.js | 378 ++++
js/color/color-mimicry.js | 90 +
.../color/color-picker.css | 0
.../color/color-picker.js | 121 +-
.../colorview.js => js/color/color-view.js | 22 +-
{vendor-overwrites => js}/csslint/LICENSE | 0
{vendor-overwrites => js}/csslint/README.md | 0
js/csslint/csslint.js | 1766 ++++++++++++++++
.../csslint/parserlib.js | 13 +-
{manage => js/dlg}/config-dialog.css | 0
js/dlg/config-dialog.js | 457 +++++
msgbox/msgbox.css => js/dlg/message-box.css | 0
js/dlg/message-box.js | 207 ++
js/dom.js | 866 ++++----
js/localization.js | 282 +--
js/messaging.js | 465 -----
js/meta-parser.js | 62 +-
js/moz-parser.js | 268 +--
js/msg.js | 130 +-
js/polyfill.js | 244 ++-
js/prefs.js | 70 +-
js/router.js | 93 +-
js/script-loader.js | 50 -
js/sections-util.js | 170 +-
js/storage-util.js | 30 +-
js/toolbox.js | 433 ++++
js/usercss.js | 81 -
js/worker-util.js | 176 +-
manage.html | 36 +-
manage/config-dialog.js | 453 -----
manage/events.js | 304 +++
manage/filters.js | 575 +++---
manage/import-export.js | 599 +++---
manage/incremental-search.js | 22 +-
manage/manage.js | 879 +-------
manage/new-ui.js | 53 +
manage/object-diff.js | 40 -
manage/render.js | 407 ++++
manage/{sort.js => sorter.js} | 144 +-
manage/updater-ui.js | 506 ++---
manifest.json | 32 +-
msgbox/msgbox.js | 185 --
options.html | 13 +-
options/options.js | 626 +++---
popup.html | 29 +-
popup/events.js | 226 +++
popup/hotkeys.js | 52 +-
popup/popup-preinit.js | 87 -
popup/popup.css | 4 +
popup/popup.js | 1024 ++++------
popup/preinit.js | 85 +
popup/{search-results.css => search.css} | 9 +-
popup/{search-results.js => search.js} | 63 +-
tools/build-vendor.js | 12 +-
.../codemirror-addon/match-highlighter.js | 5 +-
.../colorpicker/colorconverter.js | 371 ----
vendor-overwrites/csslint/csslint.js | 1778 -----------------
127 files changed, 12217 insertions(+), 11867 deletions(-)
create mode 100644 background/background-api.js
create mode 100644 background/browser-cmd-hotkeys.js
create mode 100644 background/remove-unused-storage.js
create mode 100644 edit/autocomplete.js
create mode 100644 edit/editor.js
rename edit/{show-keymap-help.js => keymap-help.js} (65%)
delete mode 100644 edit/linter-config-dialog.js
delete mode 100644 edit/linter-defaults.js
create mode 100644 edit/linter-dialogs.js
delete mode 100644 edit/linter-engines.js
delete mode 100644 edit/linter-help-dialog.js
create mode 100644 edit/linter-manager.js
delete mode 100644 edit/linter-meta.js
delete mode 100644 edit/linter.js
create mode 100644 edit/preinit.js
create mode 100644 install-usercss/preinit.js
rename {vendor-overwrites/colorpicker => js/color}/LICENSE (100%)
rename {vendor-overwrites/colorpicker => js/color}/README.md (100%)
create mode 100644 js/color/color-converter.js
create mode 100644 js/color/color-mimicry.js
rename vendor-overwrites/colorpicker/colorpicker.css => js/color/color-picker.css (100%)
rename vendor-overwrites/colorpicker/colorpicker.js => js/color/color-picker.js (89%)
rename vendor-overwrites/colorpicker/colorview.js => js/color/color-view.js (98%)
rename {vendor-overwrites => js}/csslint/LICENSE (100%)
rename {vendor-overwrites => js}/csslint/README.md (100%)
create mode 100644 js/csslint/csslint.js
rename {vendor-overwrites => js}/csslint/parserlib.js (99%)
rename {manage => js/dlg}/config-dialog.css (100%)
create mode 100644 js/dlg/config-dialog.js
rename msgbox/msgbox.css => js/dlg/message-box.css (100%)
create mode 100644 js/dlg/message-box.js
delete mode 100644 js/messaging.js
delete mode 100644 js/script-loader.js
create mode 100644 js/toolbox.js
delete mode 100644 js/usercss.js
delete mode 100644 manage/config-dialog.js
create mode 100644 manage/events.js
create mode 100644 manage/new-ui.js
delete mode 100644 manage/object-diff.js
create mode 100644 manage/render.js
rename manage/{sort.js => sorter.js} (65%)
delete mode 100644 msgbox/msgbox.js
create mode 100644 popup/events.js
delete mode 100644 popup/popup-preinit.js
create mode 100644 popup/preinit.js
rename popup/{search-results.css => search.css} (98%)
mode change 100755 => 100644
rename popup/{search-results.js => search.js} (94%)
mode change 100755 => 100644
delete mode 100644 vendor-overwrites/colorpicker/colorconverter.js
delete mode 100644 vendor-overwrites/csslint/csslint.js
diff --git a/.eslintignore b/.eslintignore
index 8e747f5b..a710e413 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,2 @@
vendor/
-vendor-overwrites/*
-!vendor-overwrites/colorpicker
-!vendor-overwrites/csslint
+vendor-overwrites/
diff --git a/.eslintrc.yml b/.eslintrc.yml
index c99504b4..3d93786a 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -8,6 +8,9 @@ env:
es6: true
webextensions: true
+globals:
+ define: readonly
+
rules:
accessor-pairs: [2]
array-bracket-spacing: [2, never]
@@ -42,7 +45,15 @@ rules:
id-blacklist: [0]
id-length: [0]
id-match: [0]
- indent-legacy: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}]
+ indent: [2, 2, {
+ SwitchCase: 1,
+ ignoreComments: true,
+ ignoredNodes: [
+ "TemplateLiteral > *",
+ "ConditionalExpression",
+ "ForStatement"
+ ]
+ }]
jsx-quotes: [0]
key-spacing: [0]
keyword-spacing: [2]
@@ -86,7 +97,7 @@ rules:
no-empty: [2, {allowEmptyCatch: true}]
no-eq-null: [0]
no-eval: [2]
- no-ex-assign: [2]
+ no-ex-assign: [0]
no-extend-native: [2]
no-extra-bind: [2]
no-extra-boolean-cast: [2]
@@ -136,6 +147,9 @@ rules:
no-proto: [2]
no-redeclare: [2]
no-regex-spaces: [2]
+ no-restricted-globals: [2, name, event]
+ # `name` and `event` (in Chrome) are built-in globals
+ # but we don't use these globals so it's most likely a mistake/typo
no-restricted-imports: [0]
no-restricted-modules: [2, domain, freelist, smalloc, sys]
no-restricted-syntax: [2, WithStatement]
@@ -165,7 +179,7 @@ rules:
no-unsafe-negation: [2]
no-unused-expressions: [1]
no-unused-labels: [0]
- no-unused-vars: [2, {args: after-used}]
+ no-unused-vars: [2, {args: after-used, argsIgnorePattern: "^require$"}]
no-use-before-define: [2, nofunc]
no-useless-call: [2]
no-useless-computed-key: [2]
@@ -220,3 +234,7 @@ overrides:
webextensions: false
parserOptions:
ecmaVersion: 2017
+
+ - files: ["**/*worker*.js"]
+ env:
+ worker: true
diff --git a/README.md b/README.md
index 16d56267..b731931d 100644
--- a/README.md
+++ b/README.md
@@ -47,15 +47,15 @@ See our [contributing](./.github/CONTRIBUTING.md) page for more details.
## License
-Inherited code from the original [Stylish](https://github.com/stylish-userstyles/stylish/):
+Inherited code from the original [Stylish](https://github.com/stylish-userstyles/stylish/):
Copyright © 2005-2014 [Jason Barnabe](jason.barnabe@gmail.com)
-Current Stylus:
+Current Stylus:
Copyright © 2017-2019 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
-**[GNU GPLv3](./LICENSE)**
+**[GNU GPLv3](./LICENSE)**
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/background/background-api.js b/background/background-api.js
new file mode 100644
index 00000000..8e6a066a
--- /dev/null
+++ b/background/background-api.js
@@ -0,0 +1,154 @@
+'use strict';
+
+/* Populates API */
+
+define(require => {
+ const {
+ URLS,
+ activateTab,
+ findExistingTab,
+ getActiveTab,
+ isTabReplaceable,
+ openURL,
+ } = require('/js/toolbox');
+ const {API, msg} = require('/js/msg');
+ const {createWorker} = require('/js/worker-util');
+ const prefs = require('/js/prefs');
+
+ Object.assign(API, ...[
+ require('./icon-manager'),
+ require('./openusercss-api'),
+ require('./search-db'),
+ ], /** @namespace API */ {
+
+ browserCommands: {
+ openManage: () => API.openManage(),
+ openOptions: () => API.openManage({options: true}),
+ reload: () => chrome.runtime.reload(),
+ styleDisableAll(info) {
+ prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
+ },
+ },
+
+ /** @type {StyleManager} */
+ styles: require('./style-manager'),
+
+ /** @type {Sync} */
+ sync: require('./sync'),
+
+ /** @type {StyleUpdater} */
+ updater: require('./update'),
+
+ /** @type {UsercssHelper} */
+ usercss: Object.assign({},
+ require('./usercss-api-helper'),
+ require('./usercss-install-helper')),
+
+ /** @type {BackgroundWorker} */
+ worker: createWorker({
+ url: '/background/background-worker.js',
+ }),
+
+ /** @returns {string} */
+ getTabUrlPrefix() {
+ const {url} = this.sender.tab;
+ if (url.startsWith(URLS.ownOrigin)) {
+ return 'stylus';
+ }
+ return url.match(/^([\w-]+:\/+[^/#]+)/)[1];
+ },
+
+ /** @returns {PrefsValues} */
+ getPrefs: () => prefs.values,
+ setPref(key, value) {
+ prefs.set(key, value);
+ },
+
+ /**
+ * Opens the editor or activates an existing tab
+ * @param {{
+ id?: number
+ domain?: string
+ 'url-prefix'?: string
+ }} params
+ * @returns {Promise}
+ */
+ openEditor(params) {
+ const u = new URL(chrome.runtime.getURL('edit.html'));
+ u.search = new URLSearchParams(params);
+ return openURL({
+ url: `${u}`,
+ currentWindow: null,
+ newWindow: prefs.get('openEditInWindow') && Object.assign({},
+ prefs.get('openEditInWindow.popup') && {type: 'popup'},
+ prefs.get('windowPosition')),
+ });
+ },
+
+ /** @returns {Promise} */
+ async openManage({options = false, search, searchMode} = {}) {
+ let url = chrome.runtime.getURL('manage.html');
+ if (search) {
+ url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
+ }
+ if (options) {
+ url += '#stylus-options';
+ }
+ let tab = await findExistingTab({
+ url,
+ currentWindow: null,
+ ignoreHash: true,
+ ignoreSearch: true,
+ });
+ if (tab) {
+ await activateTab(tab);
+ if (url !== (tab.pendingUrl || tab.url)) {
+ await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
+ }
+ return tab;
+ }
+ tab = await getActiveTab();
+ return isTabReplaceable(tab, url)
+ ? activateTab(tab, {url})
+ : browser.tabs.create({url}).then(activateTab); // activateTab unminimizes the window
+ },
+
+ /**
+ * Same as openURL, the only extra prop in `opts` is `message` - it'll be sent
+ * when the tab is ready, which is needed in the popup, otherwise another
+ * extension could force the tab to open in foreground thus auto-closing the
+ * popup (in Chrome at least) and preventing the sendMessage code from running
+ * @returns {Promise}
+ */
+ async openURL(opts) {
+ const tab = await openURL(opts);
+ if (opts.message) {
+ await onTabReady(tab);
+ await msg.sendTab(tab.id, opts.message);
+ }
+ return tab;
+ function onTabReady(tab) {
+ return new Promise((resolve, reject) =>
+ setTimeout(function ping(numTries = 10, delay = 100) {
+ msg.sendTab(tab.id, {method: 'ping'})
+ .catch(() => false)
+ .then(pong => pong
+ ? resolve(tab)
+ : numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
+ reject('timeout'));
+ }));
+ }
+ },
+ });
+
+ msg.on((msg, sender) => {
+ if (msg.method === 'invokeAPI') {
+ const fn = msg.path.reduce((res, name) => res && res[name], API);
+ if (!fn) throw new Error(`Unknown API.${msg.path.join('.')}`);
+ const res = typeof fn === 'function'
+ ? fn.apply({msg, sender}, msg.args)
+ : fn;
+ return res === undefined ? null : res;
+ }
+ });
+});
diff --git a/background/background-worker.js b/background/background-worker.js
index 5c8136b4..94430d13 100644
--- a/background/background-worker.js
+++ b/background/background-worker.js
@@ -1,84 +1,44 @@
-/* global workerUtil importScripts parseMozFormat metaParser styleCodeEmpty colorConverter */
'use strict';
-importScripts('/js/worker-util.js');
-const {loadScript} = workerUtil;
+define(require => { // define and require use `importScripts` which is synchronous
+ const {createAPI} = require('/js/worker-util');
-/** @namespace ApiWorker */
-workerUtil.createAPI({
- parseMozFormat(arg) {
- loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
- return parseMozFormat(arg);
- },
- compileUsercss,
- parseUsercssMeta(text, indexOffset = 0) {
- loadScript(
- '/vendor/usercss-meta/usercss-meta.min.js',
- '/vendor-overwrites/colorpicker/colorconverter.js',
- '/js/meta-parser.js'
- );
- return metaParser.parse(text, indexOffset);
- },
- nullifyInvalidVars(vars) {
- loadScript(
- '/vendor/usercss-meta/usercss-meta.min.js',
- '/vendor-overwrites/colorpicker/colorconverter.js',
- '/js/meta-parser.js'
- );
- return metaParser.nullifyInvalidVars(vars);
- },
-});
+ let BUILDERS;
+ const bgw = /** @namespace BackgroundWorker */ {
-function compileUsercss(preprocessor, code, vars) {
- loadScript(
- '/vendor-overwrites/csslint/parserlib.js',
- '/vendor-overwrites/colorpicker/colorconverter.js',
- '/js/moz-parser.js'
- );
- const builder = getUsercssCompiler(preprocessor);
- vars = simpleVars(vars);
- return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code)
- .then(code => parseMozFormat({code}))
- .then(({sections, errors}) => {
- if (builder.postprocess) {
- builder.postprocess(sections, vars);
- }
- return {sections, errors};
- });
+ async compileUsercss(preprocessor, code, vars) {
+ if (!BUILDERS) createBuilders();
+ const builder = BUILDERS[preprocessor] || BUILDERS.default;
+ if (!builder) throw new Error(`Unknown preprocessor "${preprocessor}"`);
+ vars = simplifyVars(vars);
+ const {preprocess, postprocess} = builder;
+ if (preprocess) code = await preprocess(code, vars);
+ const res = bgw.parseMozFormat({code});
+ if (postprocess) postprocess(res.sections, vars);
+ return res;
+ },
- function simpleVars(vars) {
- if (!vars) {
- return {};
- }
- // simplify vars by merging `va.default` to `va.value`, so BUILDER don't
- // need to test each va's default value.
- return Object.keys(vars).reduce((output, key) => {
- const va = vars[key];
- output[key] = Object.assign({}, va, {
- value: va.value === null || va.value === undefined ?
- getVarValue(va, 'default') : getVarValue(va, 'value'),
- });
- return output;
- }, {});
- }
+ parseMozFormat(...args) {
+ return require('/js/moz-parser').extractSections(...args);
+ },
- function getVarValue(va, prop) {
- if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
- // TODO: handle customized image
- return va.options.find(o => o.name === va[prop]).value;
- }
- if ((va.type === 'number' || va.type === 'range') && va.units) {
- return va[prop] + va.units;
- }
- return va[prop];
- }
-}
+ parseUsercssMeta(text) {
+ return require('/js/meta-parser').parse(text);
+ },
-function getUsercssCompiler(preprocessor) {
- const BUILDER = {
- default: {
+ nullifyInvalidVars(vars) {
+ return require('/js/meta-parser').nullifyInvalidVars(vars);
+ },
+ };
+
+ createAPI(bgw);
+
+ function createBuilders() {
+ BUILDERS = Object.assign(Object.create(null));
+
+ BUILDERS.default = {
postprocess(sections, vars) {
- loadScript('/js/sections-util.js');
+ const {styleCodeEmpty} = require('/js/sections-util');
let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
if (!varDef) return;
varDef = ':root {\n' + varDef + '}\n';
@@ -88,18 +48,20 @@ function getUsercssCompiler(preprocessor) {
}
}
},
- },
- stylus: {
+ };
+
+ BUILDERS.stylus = {
preprocess(source, vars) {
- loadScript('/vendor/stylus-lang-bundle/stylus-renderer.min.js');
+ require('/vendor/stylus-lang-bundle/stylus-renderer.min');
return new Promise((resolve, reject) => {
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
new self.StylusRenderer(varDef + source)
.render((err, output) => err ? reject(err) : resolve(output));
});
},
- },
- less: {
+ };
+
+ BUILDERS.less = {
preprocess(source, vars) {
if (!self.less) {
self.less = {
@@ -107,17 +69,18 @@ function getUsercssCompiler(preprocessor) {
useFileCache: false,
};
}
- loadScript('/vendor/less-bundle/less.min.js');
+ require('/vendor/less-bundle/less.min');
const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
return self.less.render(varDefs + source)
.then(({css}) => css);
},
- },
- uso: {
- preprocess(source, vars) {
- loadScript('/vendor-overwrites/colorpicker/colorconverter.js');
+ };
+
+ BUILDERS.uso = {
+ async preprocess(source, vars) {
+ const colorConverter = require('/js/color/color-converter');
const pool = new Map();
- return Promise.resolve(doReplace(source));
+ return doReplace(source);
function getValue(name, rgbName) {
if (!vars.hasOwnProperty(name)) {
@@ -164,14 +127,35 @@ function getUsercssCompiler(preprocessor) {
});
}
},
- },
- };
-
- if (preprocessor) {
- if (!BUILDER[preprocessor]) {
- throw new Error('unknwon preprocessor');
- }
- return BUILDER[preprocessor];
+ };
}
- return BUILDER.default;
-}
+
+ function getVarValue(va, prop) {
+ if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
+ // TODO: handle customized image
+ return va.options.find(o => o.name === va[prop]).value;
+ }
+ if ((va.type === 'number' || va.type === 'range') && va.units) {
+ return va[prop] + va.units;
+ }
+ return va[prop];
+ }
+
+ function simplifyVars(vars) {
+ if (!vars) {
+ return {};
+ }
+ // simplify vars by merging `va.default` to `va.value`, so BUILDER don't
+ // need to test each va's default value.
+ return Object.keys(vars).reduce((output, key) => {
+ const va = vars[key];
+ output[key] = Object.assign({}, va, {
+ value: va.value === null || va.value === undefined ?
+ getVarValue(va, 'default') : getVarValue(va, 'value'),
+ });
+ return output;
+ }, {});
+ }
+
+ return bgw;
+});
diff --git a/background/background.js b/background/background.js
index 35690c16..91b472ea 100644
--- a/background/background.js
+++ b/background/background.js
@@ -1,178 +1,39 @@
-/* global
- activateTab
- API
- chromeLocal
- findExistingTab
- FIREFOX
- getActiveTab
- isTabReplaceable
- msg
- openURL
- prefs
- semverCompare
- URLS
- workerUtil
-*/
'use strict';
-//#region API
+define(require => {
+ const {FIREFOX} = require('/js/toolbox');
+ const {API, msg} = require('/js/msg');
+ const styleManager = require('./style-manager');
+ require('./background-api');
-Object.assign(API, {
-
- /** @type {ApiWorker} */
- worker: workerUtil.createWorker({
- url: '/background/background-worker.js',
- }),
-
- /** @returns {string} */
- getTabUrlPrefix() {
- const {url} = this.sender.tab;
- if (url.startsWith(URLS.ownOrigin)) {
- return 'stylus';
- }
- return url.match(/^([\w-]+:\/+[^/#]+)/)[1];
- },
-
- /** @returns {Prefs} */
- getPrefs: () => prefs.values,
- setPref(key, value) {
- prefs.set(key, value);
- },
-
- /**
- * Opens the editor or activates an existing tab
- * @param {{
- id?: number
- domain?: string
- 'url-prefix'?: string
- }} params
- * @returns {Promise}
- */
- openEditor(params) {
- const u = new URL(chrome.runtime.getURL('edit.html'));
- u.search = new URLSearchParams(params);
- return openURL({
- url: `${u}`,
- currentWindow: null,
- newWindow: prefs.get('openEditInWindow') && Object.assign({},
- prefs.get('openEditInWindow.popup') && {type: 'popup'},
- prefs.get('windowPosition')),
- });
- },
-
- /** @returns {Promise} */
- async openManage({options = false, search, searchMode} = {}) {
- let url = chrome.runtime.getURL('manage.html');
- if (search) {
- url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
- }
- if (options) {
- url += '#stylus-options';
- }
- let tab = await findExistingTab({
- url,
- currentWindow: null,
- ignoreHash: true,
- ignoreSearch: true,
- });
- if (tab) {
- await activateTab(tab);
- if (url !== (tab.pendingUrl || tab.url)) {
- await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
- }
- return tab;
- }
- tab = await getActiveTab();
- return isTabReplaceable(tab, url)
- ? activateTab(tab, {url})
- : browser.tabs.create({url}).then(activateTab); // activateTab unminimizes the window
- },
-
- /**
- * Same as openURL, the only extra prop in `opts` is `message` - it'll be sent
- * when the tab is ready, which is needed in the popup, otherwise another
- * extension could force the tab to open in foreground thus auto-closing the
- * popup (in Chrome at least) and preventing the sendMessage code from running
- * @returns {Promise}
- */
- async openURL(opts) {
- const tab = await openURL(opts);
- if (opts.message) {
- await onTabReady(tab);
- await msg.sendTab(tab.id, opts.message);
- }
- return tab;
- function onTabReady(tab) {
- return new Promise((resolve, reject) =>
- setTimeout(function ping(numTries = 10, delay = 100) {
- msg.sendTab(tab.id, {method: 'ping'})
- .catch(() => false)
- .then(pong => pong
- ? resolve(tab)
- : numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
- reject('timeout'));
- }));
- }
- },
-});
-
-//#endregion
-//#region browserCommands
-
-const browserCommands = {
- openManage: () => API.openManage(),
- openOptions: () => API.openManage({options: true}),
- styleDisableAll(info) {
- prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
- },
- reload: () => chrome.runtime.reload(),
-};
-if (chrome.commands) {
- chrome.commands.onCommand.addListener(command => browserCommands[command]());
-}
-if (FIREFOX && browser.commands && browser.commands.update) {
- // register hotkeys in FF
- const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
- prefs.subscribe(hotkeyPrefs, (name, value) => {
- try {
- name = name.split('.')[1];
- if (value.trim()) {
- browser.commands.update({name, shortcut: value});
- } else {
- browser.commands.reset(name);
- }
- } catch (e) {}
+ // These are loaded conditionally.
+ // Each item uses `require` individually so IDE can jump to the source and track usage.
+ Promise.all([
+ FIREFOX &&
+ require(['./style-via-api']),
+ FIREFOX && ((browser.commands || {}).update) &&
+ require(['./browser-cmd-hotkeys']),
+ !FIREFOX &&
+ require(['./content-scripts']),
+ !FIREFOX &&
+ require(['./style-via-webrequest']),
+ chrome.contextMenus &&
+ require(['./context-menus']),
+ styleManager.ready,
+ ]).then(() => {
+ msg.isBgReady = true;
+ msg.broadcast({method: 'backgroundReady'});
});
-}
-//#endregion
-//#region Init
-
-msg.on((msg, sender) => {
- if (msg.method === 'invokeAPI') {
- const fn = msg.path.reduce((res, name) => res && res[name], API);
- if (!fn) throw new Error(`Unknown API.${msg.path.join('.')}`);
- const res = fn.apply({msg, sender}, msg.args);
- return res === undefined ? null : res;
+ if (chrome.commands) {
+ chrome.commands.onCommand.addListener(id => API.browserCommands[id]());
}
+
+ chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
+ if (reason !== 'update') return;
+ const [a, b, c] = (previousVersion || '').split('.');
+ if (a <= 1 && b <= 5 && c <= 13) { // 1.5.13
+ require(['./remove-unused-storage']);
+ }
+ });
});
-
-chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
- if (reason !== 'update') return;
- if (semverCompare(previousVersion, '1.5.13') <= 0) {
- // Removing unused stuff
- // TODO: delete this entire block by the middle of 2021
- try {
- localStorage.clear();
- } catch (e) {}
- setTimeout(async () => {
- const del = Object.keys(await chromeLocal.get())
- .filter(key => key.startsWith('usoSearchCache'));
- if (del.length) chromeLocal.remove(del);
- }, 15e3);
- }
-});
-
-msg.broadcast({method: 'backgroundReady'});
-
-//#endregion
diff --git a/background/browser-cmd-hotkeys.js b/background/browser-cmd-hotkeys.js
new file mode 100644
index 00000000..96587721
--- /dev/null
+++ b/background/browser-cmd-hotkeys.js
@@ -0,0 +1,23 @@
+'use strict';
+
+/*
+ Registers hotkeys in FF
+ */
+
+define(require => {
+ const prefs = require('/js/prefs');
+
+ const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
+ prefs.subscribe(hotkeyPrefs, updateHotkey, {runNow: true});
+
+ async function updateHotkey(name, value) {
+ try {
+ name = name.split('.')[1];
+ if (value.trim()) {
+ await browser.commands.update({name, shortcut: value});
+ } else {
+ await browser.commands.reset(name);
+ }
+ } catch (e) {}
+ }
+});
diff --git a/background/content-scripts.js b/background/content-scripts.js
index 08b7d144..ad05c03c 100644
--- a/background/content-scripts.js
+++ b/background/content-scripts.js
@@ -1,25 +1,24 @@
-/* global
- FIREFOX
- ignoreChromeError
- msg
- URLS
-*/
'use strict';
/*
Reinject content scripts when the extension is reloaded/updated.
- Firefox handles this automatically.
+ Not used in Firefox as it reinjects automatically.
*/
-// eslint-disable-next-line no-unused-expressions
-!FIREFOX && (() => {
+define(require => {
+ const {
+ URLS,
+ ignoreChromeError,
+ } = require('/js/toolbox');
+ const {msg} = require('/js/msg');
+
const NTP = 'chrome://newtab/';
const ALL_URLS = '';
const SCRIPTS = chrome.runtime.getManifest().content_scripts;
// expand * as .*?
const wildcardAsRegExp = (s, flags) => new RegExp(
- s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
- .replace(/\*/g, '.*?'), flags);
+ s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
+ .replace(/\*/g, '.*?'), flags);
for (const cs of SCRIPTS) {
cs.matches = cs.matches.map(m => (
m === ALL_URLS ? m : wildcardAsRegExp(m)
@@ -118,4 +117,4 @@
function onBusyTabRemoved(tabId) {
trackBusyTab(tabId, false);
}
-})();
+});
diff --git a/background/context-menus.js b/background/context-menus.js
index b5f66d29..0fe70061 100644
--- a/background/context-menus.js
+++ b/background/context-menus.js
@@ -1,16 +1,15 @@
-/* global
- browserCommands
- CHROME
- FIREFOX
- ignoreChromeError
- msg
- prefs
- URLS
-*/
'use strict';
-// eslint-disable-next-line no-unused-expressions
-chrome.contextMenus && (() => {
+define(require => {
+ const {
+ CHROME,
+ FIREFOX,
+ URLS,
+ ignoreChromeError,
+ } = require('/js/toolbox');
+ const {API, msg} = require('/js/msg');
+ const prefs = require('/js/prefs');
+
const contextMenus = {
'show-badge': {
title: 'menuShowBadge',
@@ -18,20 +17,20 @@ chrome.contextMenus && (() => {
},
'disableAll': {
title: 'disableAllStyles',
- click: browserCommands.styleDisableAll,
+ click: API.browserCommands.styleDisableAll,
},
'open-manager': {
title: 'openStylesManager',
- click: browserCommands.openManage,
+ click: API.browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
- click: browserCommands.openOptions,
+ click: API.browserCommands.openOptions,
},
'reload': {
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload',
- click: browserCommands.reload,
+ click: API.browserCommands.reload,
},
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
@@ -104,4 +103,4 @@ chrome.contextMenus && (() => {
chrome.contextMenus.remove(id, ignoreChromeError);
}
}
-})();
+});
diff --git a/background/db-chrome-storage.js b/background/db-chrome-storage.js
index 6327a54c..8cda8eb7 100644
--- a/background/db-chrome-storage.js
+++ b/background/db-chrome-storage.js
@@ -1,56 +1,59 @@
-/* global chromeLocal */
-/* exported createChromeStorageDB */
'use strict';
-function createChromeStorageDB() {
- let INC;
+define(require => {
+ const {chromeLocal} = require('/js/storage-util');
+ let INC;
const PREFIX = 'style-';
const METHODS = {
+
+ delete: id => chromeLocal.remove(PREFIX + id),
+
// FIXME: we don't use this method at all. Should we remove this?
get: id => chromeLocal.getValue(PREFIX + id),
- put: obj =>
- // FIXME: should we clone the object?
- Promise.resolve(!obj.id && prepareInc().then(() => Object.assign(obj, {id: INC++})))
- .then(() => chromeLocal.setValue(PREFIX + obj.id, obj))
- .then(() => obj.id),
- putMany: items => prepareInc()
- .then(() =>
- chromeLocal.set(items.reduce((data, item) => {
- if (!item.id) item.id = INC++;
- data[PREFIX + item.id] = item;
- return data;
- }, {})))
- .then(() => items.map(i => i.id)),
- delete: id => chromeLocal.remove(PREFIX + id),
- getAll: () => chromeLocal.get()
- .then(result => {
- const output = [];
- for (const key in result) {
- if (key.startsWith(PREFIX) && Number(key.slice(PREFIX.length))) {
- output.push(result[key]);
- }
+
+ async getAll() {
+ return Object.entries(await chromeLocal.get())
+ .map(([key, val]) => key.startsWith(PREFIX) && Number(key.slice(PREFIX.length)) && val)
+ .filter(Boolean);
+ },
+
+ async put(item) {
+ if (!item.id) {
+ if (!INC) await prepareInc();
+ item.id = INC++;
+ }
+ await chromeLocal.setValue(PREFIX + item.id, item);
+ return item.id;
+ },
+
+ async putMany(items) {
+ const data = {};
+ for (const item of items) {
+ if (!item.id) {
+ if (!INC) await prepareInc();
+ item.id = INC++;
}
- return output;
- }),
+ data[PREFIX + item.id] = item;
+ }
+ await chromeLocal.set(data);
+ return items.map(_ => _.id);
+ },
};
- return {
- exec: (method, ...args) => METHODS[method](...args),
- };
-
- function prepareInc() {
- if (INC) return Promise.resolve();
- return chromeLocal.get().then(result => {
- INC = 1;
- for (const key in result) {
- if (key.startsWith(PREFIX)) {
- const id = Number(key.slice(PREFIX.length));
- if (id >= INC) {
- INC = id + 1;
- }
+ async function prepareInc() {
+ INC = 1;
+ for (const key in await chromeLocal.get()) {
+ if (key.startsWith(PREFIX)) {
+ const id = Number(key.slice(PREFIX.length));
+ if (id >= INC) {
+ INC = id + 1;
}
}
- });
+ }
}
-}
+
+ return function dbExecChromeStorage(method, ...args) {
+ return METHODS[method](...args);
+ };
+});
diff --git a/background/db.js b/background/db.js
index 3b6f8bc3..ec96c928 100644
--- a/background/db.js
+++ b/background/db.js
@@ -1,25 +1,25 @@
-/* global chromeLocal workerUtil createChromeStorageDB */
-/* exported db */
/*
-Initialize a database. There are some problems using IndexedDB in Firefox:
-https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/
-
-Some of them are fixed in FF59:
-https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
+ Initialize a database. There are some problems using IndexedDB in Firefox:
+ https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/
+ Some of them are fixed in FF59:
+ https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
*/
'use strict';
-const db = (() => {
+define(require => {
+ const {chromeLocal} = require('/js/storage-util');
+ const {cloneError} = require('/js/worker-util');
+
const DATABASE = 'stylish';
const STORE = 'styles';
const FALLBACK = 'dbInChromeStorage';
- const dbApi = {
+ const execFn = tryUsingIndexedDB().catch(useChromeStorage);
+
+ const exports = {
async exec(...args) {
- dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
- return dbApi.exec(...args);
+ return (await execFn)(...args);
},
};
- return dbApi;
async function tryUsingIndexedDB() {
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
@@ -44,13 +44,13 @@ const db = (() => {
await dbExecIndexedDB('delete', e.id); // throws if `e` or id is null
}
- function useChromeStorage(err) {
+ async function useChromeStorage(err) {
chromeLocal.setValue(FALLBACK, true);
if (err) {
- chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err));
+ chromeLocal.setValue(FALLBACK + 'Reason', cloneError(err));
console.warn('Failed to access indexedDB. Switched to storage API.', err);
}
- return createChromeStorageDB().exec;
+ return require(['./db-chrome-storage']);
}
async function dbExecIndexedDB(method, ...args) {
@@ -90,4 +90,6 @@ const db = (() => {
});
}
}
-})();
+
+ return exports;
+});
diff --git a/background/icon-manager.js b/background/icon-manager.js
index e2781fde..a60fb209 100644
--- a/background/icon-manager.js
+++ b/background/icon-manager.js
@@ -1,11 +1,44 @@
-/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API */
-/* exported iconManager */
'use strict';
-const iconManager = (() => {
+define(require => {
+ const {
+ FIREFOX,
+ VIVALDI,
+ CHROME,
+ debounce,
+ } = require('/js/toolbox');
+ const prefs = require('/js/prefs');
+ const {
+ setBadgeBackgroundColor,
+ setBadgeText,
+ setIcon,
+ } = require('./icon-util');
+ const tabManager = require('./tab-manager');
+
const ICON_SIZES = FIREFOX || CHROME >= 55 && !VIVALDI ? [16, 32] : [19, 38];
const staleBadges = new Set();
+ let exports;
+ const {
+
+ updateIconBadge,
+
+ } = exports = /** @namespace API */ {
+ /**
+ * @param {(number|string)[]} styleIds
+ * @param {boolean} [lazyBadge=false] preventing flicker during page load
+ */
+ updateIconBadge(styleIds, {lazyBadge} = {}) {
+ // FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
+ const {frameId, tab: {id: tabId}} = this.sender;
+ const value = styleIds.length ? styleIds.map(Number) : undefined;
+ tabManager.set(tabId, 'styleIds', frameId, value);
+ debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
+ staleBadges.add(tabId);
+ if (!frameId) refreshIcon(tabId, true);
+ },
+ };
+
prefs.subscribe([
'disableAll',
'badgeDisabled',
@@ -27,21 +60,7 @@ const iconManager = (() => {
refreshAllIcons();
});
- Object.assign(API, {
- /** @param {(number|string)[]} styleIds
- * @param {boolean} [lazyBadge=false] preventing flicker during page load */
- updateIconBadge(styleIds, {lazyBadge} = {}) {
- // FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
- const {frameId, tab: {id: tabId}} = this.sender;
- const value = styleIds.length ? styleIds.map(Number) : undefined;
- tabManager.set(tabId, 'styleIds', frameId, value);
- debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
- staleBadges.add(tabId);
- if (!frameId) refreshIcon(tabId, true);
- },
- });
-
- navigatorUtil.onCommitted(({tabId, frameId}) => {
+ chrome.webNavigation.onCommitted.addListener(({tabId, frameId}) => {
if (!frameId) tabManager.set(tabId, 'styleIds', undefined);
});
@@ -53,13 +72,13 @@ const iconManager = (() => {
function onPortDisconnected({sender}) {
if (tabManager.get(sender.tab.id, 'styleIds')) {
- API.updateIconBadge.call({sender}, [], {lazyBadge: true});
+ updateIconBadge.call({sender}, [], {lazyBadge: true});
}
}
function refreshIconBadgeText(tabId) {
const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
- iconUtil.setBadgeText({tabId, text});
+ setBadgeText({tabId, text});
}
function getIconName(hasStyles = false) {
@@ -77,7 +96,7 @@ const iconManager = (() => {
return;
}
tabManager.set(tabId, 'icon', newIcon);
- iconUtil.setIcon({
+ setIcon({
path: getIconPath(newIcon),
tabId,
});
@@ -102,14 +121,14 @@ const iconManager = (() => {
}
function refreshGlobalIcon() {
- iconUtil.setIcon({
+ setIcon({
path: getIconPath(getIconName()),
});
}
function refreshIconBadgeColor() {
const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
- iconUtil.setBadgeBackgroundColor({
+ setBadgeBackgroundColor({
color,
});
}
@@ -133,4 +152,6 @@ const iconManager = (() => {
}
staleBadges.clear();
}
-})();
+
+ return exports;
+});
diff --git a/background/icon-util.js b/background/icon-util.js
index 4bfffe31..90d07b78 100644
--- a/background/icon-util.js
+++ b/background/icon-util.js
@@ -1,91 +1,73 @@
-/* global ignoreChromeError */
-/* exported iconUtil */
'use strict';
-const iconUtil = (() => {
- const canvas = document.createElement('canvas');
- const ctx = canvas.getContext('2d');
- // https://github.com/openstyles/stylus/issues/335
- let noCanvas;
+define(require => {
+ const {ignoreChromeError} = require('/js/toolbox');
+
const imageDataCache = new Map();
- // test if canvas is usable
- const canvasReady = loadImage('/images/icon/16.png')
- .then(imageData => {
- noCanvas = imageData.data.every(b => b === 255);
- });
+ // https://github.com/openstyles/stylus/issues/335
+ const hasCanvas = loadImage('/images/icon/16.png')
+ .then(({data}) => data.some(b => b !== 255));
- return extendNative({
- /*
- Cache imageData for paths
- */
- setIcon,
- setBadgeText,
- });
+ const exports = {
- function loadImage(url) {
- let result = imageDataCache.get(url);
- if (!result) {
- result = new Promise((resolve, reject) => {
- const img = new Image();
- img.src = url;
- img.onload = () => {
- const w = canvas.width = img.width;
- const h = canvas.height = img.height;
- ctx.clearRect(0, 0, w, h);
- ctx.drawImage(img, 0, 0, w, h);
- resolve(ctx.getImageData(0, 0, w, h));
- };
- img.onerror = reject;
- });
- imageDataCache.set(url, result);
- }
+ /** @param {chrome.browserAction.TabIconDetails} data */
+ async setIcon(data) {
+ if (await hasCanvas) {
+ data.imageData = {};
+ for (const [key, url] of Object.entries(data.path)) {
+ data.imageData[key] = imageDataCache.get(url) || await loadImage(url);
+ }
+ delete data.path;
+ }
+ safeCall('setIcon', data);
+ },
+
+ /** @param {chrome.browserAction.BadgeTextDetails} data */
+ setBadgeText(data) {
+ safeCall('setBadgeText', data);
+ },
+
+ /** @param {chrome.browserAction.BadgeBackgroundColorDetails} data */
+ setBadgeBackgroundColor(data) {
+ safeCall('setBadgeBackgroundColor', data);
+ },
+ };
+
+ // Caches imageData for icon paths
+ async function loadImage(url) {
+ const {OffscreenCanvas} = self.createImageBitmap && self || {};
+ const img = OffscreenCanvas
+ ? await createImageBitmap(await (await fetch(url)).blob())
+ : await new Promise((resolve, reject) =>
+ Object.assign(new Image(), {
+ src: url,
+ onload: e => resolve(e.target),
+ onerror: reject,
+ }));
+ const {width: w, height: h} = img;
+ const canvas = OffscreenCanvas
+ ? new OffscreenCanvas(w, h)
+ : Object.assign(document.createElement('canvas'), {width: w, height: h});
+ const ctx = canvas.getContext('2d');
+ ctx.drawImage(img, 0, 0, w, h);
+ const result = ctx.getImageData(0, 0, w, h);
+ imageDataCache.set(url, result);
return result;
}
- function setIcon(data) {
- canvasReady.then(() => {
- if (noCanvas) {
- chrome.browserAction.setIcon(data, ignoreChromeError);
- return;
+ function safeCall(method, data) {
+ const {browserAction = {}} = chrome;
+ const fn = browserAction[method];
+ if (fn) {
+ try {
+ // Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
+ fn.call(browserAction, data, ignoreChromeError);
+ } catch (e) {
+ // FIXME: skip pre-rendered tabs?
+ fn.call(browserAction, data);
}
- const pending = [];
- data.imageData = {};
- for (const [key, url] of Object.entries(data.path)) {
- pending.push(loadImage(url)
- .then(imageData => {
- data.imageData[key] = imageData;
- }));
- }
- Promise.all(pending).then(() => {
- delete data.path;
- chrome.browserAction.setIcon(data, ignoreChromeError);
- });
- });
- }
-
- function setBadgeText(data) {
- try {
- // Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
- chrome.browserAction.setBadgeText(data, ignoreChromeError);
- } catch (e) {
- // FIXME: skip pre-rendered tabs?
- chrome.browserAction.setBadgeText(data);
}
}
- function extendNative(target) {
- return new Proxy(target, {
- get: (target, prop) => {
- // FIXME: do we really need this?
- if (!chrome.browserAction ||
- !['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
- return () => {};
- }
- if (target[prop]) {
- return target[prop];
- }
- return chrome.browserAction[prop].bind(chrome.browserAction);
- },
- });
- }
-})();
+ return exports;
+});
diff --git a/background/navigator-util.js b/background/navigator-util.js
index bdcdbedb..b5bfe8b6 100644
--- a/background/navigator-util.js
+++ b/background/navigator-util.js
@@ -1,31 +1,27 @@
-/* global
- CHROME
- FIREFOX
- ignoreChromeError
- msg
- URLS
-*/
'use strict';
-(() => {
+define(require => {
+ const {
+ CHROME,
+ FIREFOX,
+ URLS,
+ ignoreChromeError,
+ } = require('/js/toolbox');
+ const {msg} = require('/js/msg');
+
/** @type {Set} */
const listeners = new Set();
- /** @type {NavigatorUtil} */
- const navigatorUtil = window.navigatorUtil = new Proxy({
+
+ const exports = {
onUrlChange(fn) {
listeners.add(fn);
},
- }, {
- get(target, prop) {
- return target[prop] ||
- (target = chrome.webNavigation[prop]).addListener.bind(target);
- },
- });
+ };
- navigatorUtil.onCommitted(onNavigation.bind('committed'));
- navigatorUtil.onHistoryStateUpdated(onFakeNavigation.bind('history'));
- navigatorUtil.onReferenceFragmentUpdated(onFakeNavigation.bind('hash'));
- navigatorUtil.onCommitted(runGreasyforkContentScript, {
+ chrome.webNavigation.onCommitted.addListener(onNavigation.bind('committed'));
+ chrome.webNavigation.onHistoryStateUpdated.addListener(onFakeNavigation.bind('history'));
+ chrome.webNavigation.onReferenceFragmentUpdated.addListener(onFakeNavigation.bind('hash'));
+ chrome.webNavigation.onCommitted.addListener(runGreasyforkContentScript, {
// expose style version on greasyfork/sleazyfork 1) info page and 2) code page
url: ['greasyfork', 'sleazyfork'].map(host => ({
hostEquals: host + '.org',
@@ -33,7 +29,7 @@
})),
});
if (FIREFOX) {
- navigatorUtil.onDOMContentLoaded(runMainContentScripts, {
+ chrome.webNavigation.onDOMContentLoaded.addListener(runMainContentScripts, {
url: [{
urlEquals: 'about:blank',
}],
@@ -84,20 +80,6 @@
runAt: 'document_start',
});
}
-})();
-/**
- * @typedef NavigatorUtil
- * @property {NavigatorUtilEvent} onBeforeNavigate
- * @property {NavigatorUtilEvent} onCommitted
- * @property {NavigatorUtilEvent} onCompleted
- * @property {NavigatorUtilEvent} onCreatedNavigationTarget
- * @property {NavigatorUtilEvent} onDOMContentLoaded
- * @property {NavigatorUtilEvent} onErrorOccurred
- * @property {NavigatorUtilEvent} onHistoryStateUpdated
- * @property {NavigatorUtilEvent} onReferenceFragmentUpdated
- * @property {NavigatorUtilEvent} onTabReplaced
-*/
-/**
- * @typedef {function(cb: function, filters: WebNavigationEventFilter?)} NavigatorUtilEvent
- */
+ return exports;
+});
diff --git a/background/openusercss-api.js b/background/openusercss-api.js
index 73a3ec3c..c947f91d 100644
--- a/background/openusercss-api.js
+++ b/background/openusercss-api.js
@@ -1,7 +1,6 @@
-/* global API */
'use strict';
-(() => {
+define(require => {
// begin:nanographql - Tiny graphQL client library
// Author: yoshuawuyts (https://github.com/yoshuawuyts)
// License: MIT
@@ -37,11 +36,10 @@
body: query({
id,
}),
- })
- .then(res => res.json());
+ }).then(res => res.json());
};
- API.openusercss = {
+ const exports = /** @namespace API */ {
/**
* This function can be used to retrieve a theme object from the
* GraphQL API, set above
@@ -100,4 +98,6 @@
}
`),
};
-})();
+
+ return exports;
+});
diff --git a/background/remove-unused-storage.js b/background/remove-unused-storage.js
new file mode 100644
index 00000000..7349e766
--- /dev/null
+++ b/background/remove-unused-storage.js
@@ -0,0 +1,25 @@
+'use strict';
+
+// Removing unused stuff from storage on extension update
+// TODO: delete this by the middle of 2021
+
+define(require => {
+ const {chromeLocal} = require('/js/storage-util');
+
+ function cleanLocalStorage() {
+ try {
+ localStorage.clear();
+ } catch (e) {}
+ }
+
+ async function cleanChromeLocal() {
+ const del = Object.keys(await chromeLocal.get())
+ .filter(key => key.startsWith('usoSearchCache'));
+ if (del.length) chromeLocal.remove(del);
+ }
+
+ return () => {
+ cleanLocalStorage();
+ setTimeout(cleanChromeLocal, 15e3);
+ };
+});
diff --git a/background/search-db.js b/background/search-db.js
index b23679ed..d774297d 100644
--- a/background/search-db.js
+++ b/background/search-db.js
@@ -1,74 +1,69 @@
-/* global
- API
- debounce
- stringAsRegExp
- tryRegExp
- usercss
-*/
'use strict';
-(() => {
+define(require => {
+ const {
+ debounce,
+ stringAsRegExp,
+ tryRegExp,
+ } = require('/js/toolbox');
+ const {API} = require('/js/msg');
+
// toLocaleLowerCase cache, autocleared after 1 minute
const cache = new Map();
const METAKEYS = ['customName', 'name', 'url', 'installationUrl', 'updateUrl'];
+ const MODES = createModes();
- const extractMeta = style =>
- style.usercssData
- ? (style.sourceCode.match(usercss.RX_META) || [''])[0]
- : null;
+ const exports = /** @namespace API */ {
+ /**
+ * @param params
+ * @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
+ * @param {'name'|'meta'|'code'|'all'|'url'} [params.mode=all]
+ * @param {number[]} [params.ids] - if not specified, all styles are searched
+ * @returns {number[]} - array of matched styles ids
+ */
+ async searchDB({query, mode = 'all', ids}) {
+ let res = [];
+ if (mode === 'url' && query) {
+ res = (await API.styles.getByUrl(query)).map(r => r.style.id);
+ } else if (mode in MODES) {
+ const modeHandler = MODES[mode];
+ const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query);
+ const rx = m && tryRegExp(m[1], m[2]);
+ const test = rx ? rx.test.bind(rx) : createTester(query);
+ res = (await API.styles.getAll())
+ .filter(style =>
+ (!ids || ids.includes(style.id)) &&
+ (!query || modeHandler(style, test)))
+ .map(style => style.id);
+ if (cache.size) debounce(clearCache, 60e3);
+ }
+ return res;
+ },
+ };
- const stripMeta = style =>
- style.usercssData
- ? style.sourceCode.replace(usercss.RX_META, '')
- : null;
+ function createModes() {
+ return Object.assign(Object.create(null), {
+ code: (style, test) =>
+ style.usercssData
+ ? test(stripMeta(style))
+ : searchSections(style, test, 'code'),
- const MODES = Object.assign(Object.create(null), {
- code: (style, test) =>
- style.usercssData
- ? test(stripMeta(style))
- : searchSections(style, test, 'code'),
-
- meta: (style, test, part) =>
- METAKEYS.some(key => test(style[key])) ||
+ meta: (style, test, part) =>
+ METAKEYS.some(key => test(style[key])) ||
test(part === 'all' ? style.sourceCode : extractMeta(style)) ||
searchSections(style, test, 'funcs'),
- name: (style, test) =>
- test(style.customName) ||
- test(style.name),
+ name: (style, test) =>
+ test(style.customName) ||
+ test(style.name),
- all: (style, test) =>
- MODES.meta(style, test, 'all') ||
- !style.usercssData && MODES.code(style, test),
- });
+ all: (style, test) =>
+ MODES.meta(style, test, 'all') ||
+ !style.usercssData && MODES.code(style, test),
+ });
+ }
- /**
- * @param params
- * @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
- * @param {'name'|'meta'|'code'|'all'|'url'} [params.mode=all]
- * @param {number[]} [params.ids] - if not specified, all styles are searched
- * @returns {number[]} - array of matched styles ids
- */
- API.searchDB = async ({query, mode = 'all', ids}) => {
- let res = [];
- if (mode === 'url' && query) {
- res = (await API.styles.getByUrl(query)).map(r => r.style.id);
- } else if (mode in MODES) {
- const modeHandler = MODES[mode];
- const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query);
- const rx = m && tryRegExp(m[1], m[2]);
- const test = rx ? rx.test.bind(rx) : makeTester(query);
- res = (await API.styles.getAll())
- .filter(style =>
- (!ids || ids.includes(style.id)) &&
- (!query || modeHandler(style, test)))
- .map(style => style.id);
- if (cache.size) debounce(clearCache, 60e3);
- }
- return res;
- };
-
- function makeTester(query) {
+ function createTester(query) {
const flags = `u${lower(query) === query ? 'i' : ''}`;
const words = query
.split(/(".*?")|\s+/)
@@ -105,4 +100,18 @@
function clearCache() {
cache.clear();
}
-})();
+
+ function extractMeta(style) {
+ return style.usercssData
+ ? (style.sourceCode.match(API.usercss.rxMETA) || [''])[0]
+ : null;
+ }
+
+ function stripMeta(style) {
+ return style.usercssData
+ ? style.sourceCode.replace(API.usercss.rxMETA, '')
+ : null;
+ }
+
+ return exports;
+});
diff --git a/background/style-manager.js b/background/style-manager.js
index c9497288..70239fef 100644
--- a/background/style-manager.js
+++ b/background/style-manager.js
@@ -1,17 +1,3 @@
-/* global
- API
- calcStyleDigest
- createCache
- db
- msg
- prefs
- stringAsRegExp
- styleCodeEmpty
- styleSectionGlobal
- tabManager
- tryRegExp
- URLS
-*/
'use strict';
/*
@@ -23,10 +9,25 @@ The live preview feature relies on `runtime.connect` and `port.onDisconnect`
to cleanup the temporary code. See /edit/live-preview.js.
*/
-/* exported styleManager */
-const styleManager = API.styles = (() => {
+define(require => {
+ const {
+ stringAsRegExp,
+ tryRegExp,
+ URLS,
+ } = require('/js/toolbox');
+ const {API, msg} = require('/js/msg');
+ const {
+ calcStyleDigest,
+ styleCodeEmpty,
+ styleSectionGlobal,
+ } = require('/js/sections-util');
+ const createCache = require('/js/cache');
+ const prefs = require('/js/prefs');
+ const db = require('./db');
+ const tabManager = require('./tab-manager');
//#region Declarations
+
const ready = init();
/**
* @typedef StyleMapData
@@ -40,7 +41,7 @@ const styleManager = API.styles = (() => {
/** @typedef {Object} StyleSectionsToApply */
/** @type {Map, sections: StyleSectionsToApply}>} */
const cachedStyleForUrl = createCache({
- onDeleted: (url, cache) => {
+ onDeleted(url, cache) {
for (const section of Object.values(cache.sections)) {
const data = id2data(section.id);
if (data) data.appliesTo.delete(url);
@@ -51,36 +52,30 @@ const styleManager = API.styles = (() => {
const compileRe = createCompiler(text => `^(${text})$`);
const compileSloppyRe = createCompiler(text => `^${text}$`);
const compileExclusion = createCompiler(buildExclusion);
- const DUMMY_URL = {
- hash: '',
- host: '',
- hostname: '',
- href: '',
- origin: '',
- password: '',
- pathname: '',
- port: '',
- protocol: '',
- search: '',
- searchParams: new URLSearchParams(),
- username: '',
- };
const MISSING_PROPS = {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now(),
};
const DELETE_IF_NULL = ['id', 'customName'];
- //#endregion
chrome.runtime.onConnect.addListener(handleLivePreview);
- //#region Public surface
+ //#endregion
+ //#region Exports
- // Sorted alphabetically
- return {
+ /** @type {StyleManager} */
+ const styleManager = /** @namespace StyleManager */ {
- compareRevision,
+ /* props first,
+ then method shorthands if any,
+ then inlined methods sorted alphabetically */
+
+ ready,
+
+ compareRevision(rev1, rev2) { // TODO: move somewhere else so it doesn't pollute API
+ return rev1 - rev2;
+ },
/** @returns {Promise} style id */
async delete(id, reason) {
@@ -108,9 +103,9 @@ const styleManager = API.styles = (() => {
await ready;
const id = uuidIndex.get(_id);
const oldDoc = id && id2style(id);
- if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
+ if (oldDoc && styleManager.compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
- return API.styles.delete(id, 'sync');
+ return styleManager.delete(id, 'sync');
}
},
@@ -151,7 +146,7 @@ const styleManager = API.styles = (() => {
await ready;
/* Chrome hides text frament from location.href of the page e.g. #:~:text=foo
so we'll use the real URL reported by webNavigation API */
- const {tab, frameId} = this.sender;
+ const {tab, frameId} = this && this.sender || {};
url = tab && tabManager.get(tab.id, 'url', frameId) || url;
let cache = cachedStyleForUrl.get(url);
if (!cache) {
@@ -215,7 +210,7 @@ const styleManager = API.styles = (() => {
}
}
if (sectionMatched) {
- result.push(/** @namespace StylesByUrlResult */{style, excluded, sloppy});
+ result.push(/** @namespace StylesByUrlResult */ {style, excluded, sloppy});
}
}
return result;
@@ -265,7 +260,7 @@ const styleManager = API.styles = (() => {
const oldDoc = id && id2style(id);
let diff = -1;
if (oldDoc) {
- diff = compareRevision(oldDoc._rev, doc._rev);
+ diff = styleManager.compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) {
API.sync.put(oldDoc._id, oldDoc._rev);
return;
@@ -297,8 +292,8 @@ const styleManager = API.styles = (() => {
/** @returns {Promise} */
removeInclusion: removeIncludeExclude.bind(null, 'inclusions'),
};
- //#endregion
+ //#endregion
//#region Implementation
/** @returns {StyleMapData} */
@@ -318,7 +313,7 @@ const styleManager = API.styles = (() => {
/** @returns {StyleObj} */
function createNewStyle() {
- return /** @namespace StyleObj */{
+ return /** @namespace StyleObj */ {
enabled: true,
updateUrl: null,
md5Url: null,
@@ -366,10 +361,6 @@ const styleManager = API.styles = (() => {
});
}
- function compareRevision(rev1, rev2) {
- return rev1 - rev2;
- }
-
async function addIncludeExclude(type, id, rule) {
await ready;
const style = Object.assign({}, id2style(id));
@@ -661,7 +652,20 @@ const styleManager = API.styles = (() => {
try {
return new URL(url);
} catch (err) {
- return DUMMY_URL;
+ return {
+ hash: '',
+ host: '',
+ hostname: '',
+ href: '',
+ origin: '',
+ password: '',
+ pathname: '',
+ port: '',
+ protocol: '',
+ search: '',
+ searchParams: new URLSearchParams(),
+ username: '',
+ };
}
}
@@ -677,5 +681,8 @@ const styleManager = API.styles = (() => {
function hex4dashed(num, i) {
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
}
+
//#endregion
-})();
+
+ return styleManager;
+});
diff --git a/background/style-via-api.js b/background/style-via-api.js
index 7da780ae..4116abbc 100644
--- a/background/style-via-api.js
+++ b/background/style-via-api.js
@@ -1,7 +1,10 @@
-/* global API CHROME prefs */
'use strict';
-API.styleViaAPI = !CHROME && (() => {
+define(require => {
+ const {isEmptyObj} = require('/js/polyfill');
+ const {API} = require('/js/msg');
+ const prefs = require('/js/prefs');
+
const ACTIONS = {
styleApply,
styleDeleted,
@@ -11,24 +14,27 @@ API.styleViaAPI = !CHROME && (() => {
prefChanged,
updateCount,
};
- const NOP = Promise.resolve(new Error('NOP'));
+ const NOP = new Error('NOP');
const onError = () => {};
-
/* : Object
: Object
url: String, non-enumerable
: Array of strings
section code */
const cache = new Map();
-
let observingTabs = false;
- return function (request) {
- const action = ACTIONS[request.method];
- return !action ? NOP :
- action(request, this.sender)
- .catch(onError)
- .then(maybeToggleObserver);
+ const exports = /** @namespace API */ {
+ /**
+ * Uses chrome.tabs.insertCSS
+ */
+ async styleViaAPI(request) {
+ try {
+ const fn = ACTIONS[request.method];
+ return fn ? fn(request, this.sender) : NOP;
+ } catch (e) {}
+ maybeToggleObserver();
+ },
};
function updateCount(request, sender) {
@@ -125,7 +131,7 @@ API.styleViaAPI = !CHROME && (() => {
}
const {tab, frameId} = sender;
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
- if (isEmpty(frameStyles)) {
+ if (isEmptyObj(frameStyles)) {
return NOP;
}
removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
@@ -162,7 +168,7 @@ API.styleViaAPI = !CHROME && (() => {
const tabFrames = cache.get(tabId);
if (tabFrames && frameId in tabFrames) {
delete tabFrames[frameId];
- if (isEmpty(tabFrames)) {
+ if (isEmptyObj(tabFrames)) {
onTabRemoved(tabId);
}
}
@@ -178,9 +184,9 @@ API.styleViaAPI = !CHROME && (() => {
}
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
- if (isEmpty(frameStyles)) {
+ if (isEmptyObj(frameStyles)) {
delete tabFrames[frameId];
- if (isEmpty(tabFrames)) {
+ if (isEmptyObj(tabFrames)) {
cache.delete(tabId);
}
return true;
@@ -224,10 +230,5 @@ API.styleViaAPI = !CHROME && (() => {
.catch(onError);
}
- function isEmpty(obj) {
- for (const k in obj) {
- return false;
- }
- return true;
- }
-})();
+ return exports;
+});
diff --git a/background/style-via-webrequest.js b/background/style-via-webrequest.js
index 45e12d65..00e2e89b 100644
--- a/background/style-via-webrequest.js
+++ b/background/style-via-webrequest.js
@@ -1,12 +1,10 @@
-/* global
- API
- CHROME
- prefs
-*/
'use strict';
-// eslint-disable-next-line no-unused-expressions
-CHROME && (async () => {
+define(async require => {
+ const {API} = require('/js/msg');
+ const {isEmptyObj} = require('/js/polyfill');
+ const prefs = require('/js/prefs');
+
const idCSP = 'patchCsp';
const idOFF = 'disableAll';
const idXHR = 'styleViaXhr';
@@ -16,7 +14,7 @@ CHROME && (async () => {
const enabled = {};
await prefs.initializing;
- prefs.subscribe([idXHR, idOFF, idCSP], toggle, {now: true});
+ prefs.subscribe([idXHR, idOFF, idCSP], toggle, {runNow: true});
function toggle() {
const csp = prefs.get(idCSP) && !prefs.get(idOFF);
@@ -73,14 +71,17 @@ CHROME && (async () => {
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
async function prepareStyles(req) {
const sections = await API.styles.getSectionsByUrl(req.url);
- if (Object.keys(sections).length) {
- stylesToPass[req.requestId] = !enabled.xhr ? true :
- URL.createObjectURL(new Blob([JSON.stringify(sections)]))
- .slice(blobUrlPrefix.length);
+ if (!isEmptyObj(sections)) {
+ stylesToPass[req.requestId] = !enabled.xhr || makeObjectUrl(sections);
setTimeout(cleanUp, 600e3, req.requestId);
}
}
+ function makeObjectUrl(sections) {
+ const blob = new Blob([JSON.stringify(sections)]);
+ return URL.createObjectURL(blob).slice(blobUrlPrefix.length);
+ }
+
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
function modifyHeaders(req) {
const {responseHeaders} = req;
@@ -115,7 +116,7 @@ CHROME && (async () => {
patchCspSrc(src, 'img-src', 'data:', '*');
patchCspSrc(src, 'font-src', 'data:', '*');
// Allow our DOM styles
- patchCspSrc(src, 'style-src', '\'unsafe-inline\'');
+ patchCspSrc(src, 'style-src', "'unsafe-inline'");
// Allow our XHR cookies in CSP sandbox (known case: raw github urls)
if (src.sandbox && !src.sandbox.includes('allow-same-origin')) {
src.sandbox.push('allow-same-origin');
@@ -141,4 +142,4 @@ CHROME && (async () => {
delete stylesToPass[key];
if (blobId) URL.revokeObjectURL(blobUrlPrefix + blobId);
}
-})();
+});
diff --git a/background/sync.js b/background/sync.js
index be478545..64a0a528 100644
--- a/background/sync.js
+++ b/background/sync.js
@@ -1,24 +1,29 @@
-/* global
- API
- chromeLocal
- dbToCloud
- msg
- prefs
- styleManager
- tokenManager
-*/
-/* exported sync */
-
'use strict';
-const sync = API.sync = (() => {
+define(require => {
+ const {API, msg} = require('/js/msg');
+ const {chromeLocal} = require('/js/storage-util');
+ const prefs = require('/js/prefs');
+ const {compareRevision} = require('./style-manager');
+ const tokenManager = require('./token-manager');
+
+ /** @type Sync */
+ let sync;
+
+ //#region Init
+
const SYNC_DELAY = 1; // minutes
const SYNC_INTERVAL = 30; // minutes
-
- /** @typedef API.sync.Status */
- const status = {
- /** @type {'connected'|'connecting'|'disconnected'|'disconnecting'} */
- state: 'disconnected',
+ const STATES = Object.freeze({
+ connected: 'connected',
+ connecting: 'connecting',
+ disconnected: 'disconnected',
+ disconnecting: 'disconnecting',
+ });
+ const STORAGE_KEY = 'sync/state/';
+ const status = /** @namespace Sync.Status */ {
+ STATES,
+ state: STATES.disconnected,
syncing: false,
progress: null,
currentDriveName: null,
@@ -26,51 +31,14 @@ const sync = API.sync = (() => {
login: false,
};
let currentDrive;
- const ctrl = dbToCloud.dbToCloud({
- onGet(id) {
- return API.styles.getByUUID(id);
- },
- onPut(doc) {
- return API.styles.putByUUID(doc);
- },
- onDelete(id, rev) {
- return API.styles.deleteByUUID(id, rev);
- },
- async onFirstSync() {
- for (const i of await API.styles.getAll()) {
- ctrl.put(i._id, i._rev);
- }
- },
- onProgress(e) {
- if (e.phase === 'start') {
- status.syncing = true;
- } else if (e.phase === 'end') {
- status.syncing = false;
- status.progress = null;
- } else {
- status.progress = e;
- }
- emitStatusChange();
- },
- compareRevision(a, b) {
- return styleManager.compareRevision(a, b);
- },
- getState(drive) {
- const key = `sync/state/${drive.name}`;
- return chromeLocal.getValue(key);
- },
- setState(drive, state) {
- const key = `sync/state/${drive.name}`;
- return chromeLocal.setValue(key, state);
- },
- });
+ let ctrl;
const ready = prefs.initializing.then(() => {
prefs.subscribe('sync.enabled',
(_, val) => val === 'none'
? sync.stop()
: sync.start(val, true),
- {now: true});
+ {runNow: true});
});
chrome.alarms.onAlarm.addListener(info => {
@@ -79,8 +47,12 @@ const sync = API.sync = (() => {
}
});
- // Sorted alphabetically
- return {
+ //#endregion
+ //#region Exports
+
+ sync = /** @namespace Sync */ {
+
+ // sorted alphabetically
async delete(...args) {
await ready;
@@ -89,9 +61,7 @@ const sync = API.sync = (() => {
return ctrl.delete(...args);
},
- /**
- * @returns {Promise}
- */
+ /** @returns {Promise} */
async getStatus() {
return status;
},
@@ -124,8 +94,9 @@ const sync = API.sync = (() => {
return;
}
currentDrive = getDrive(name);
+ if (!ctrl) await initController();
ctrl.use(currentDrive);
- status.state = 'connecting';
+ status.state = STATES.connecting;
status.currentDriveName = currentDrive.name;
status.login = true;
emitStatusChange();
@@ -144,7 +115,7 @@ const sync = API.sync = (() => {
}
}
prefs.set('sync.enabled', name);
- status.state = 'connected';
+ status.state = STATES.connected;
schedule(SYNC_INTERVAL);
emitStatusChange();
},
@@ -155,17 +126,16 @@ const sync = API.sync = (() => {
return;
}
chrome.alarms.clear('syncNow');
- status.state = 'disconnecting';
+ status.state = STATES.disconnecting;
emitStatusChange();
try {
await ctrl.stop();
await tokenManager.revokeToken(currentDrive.name);
- await chromeLocal.remove(`sync/state/${currentDrive.name}`);
- } catch (e) {
- }
+ await chromeLocal.remove(STORAGE_KEY + currentDrive.name);
+ } catch (e) {}
currentDrive = null;
prefs.set('sync.enabled', 'none');
- status.state = 'disconnected';
+ status.state = STATES.disconnected;
status.currentDriveName = null;
status.login = false;
emitStatusChange();
@@ -186,6 +156,47 @@ const sync = API.sync = (() => {
},
};
+ //#endregion
+ //#region Utils
+
+ async function initController() {
+ await require(['js!/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */
+ ctrl = dbToCloud.dbToCloud({
+ onGet(id) {
+ return API.styles.getByUUID(id);
+ },
+ onPut(doc) {
+ return API.styles.putByUUID(doc);
+ },
+ onDelete(id, rev) {
+ return API.styles.deleteByUUID(id, rev);
+ },
+ async onFirstSync() {
+ for (const i of await API.styles.getAll()) {
+ ctrl.put(i._id, i._rev);
+ }
+ },
+ onProgress(e) {
+ if (e.phase === 'start') {
+ status.syncing = true;
+ } else if (e.phase === 'end') {
+ status.syncing = false;
+ status.progress = null;
+ } else {
+ status.progress = e;
+ }
+ emitStatusChange();
+ },
+ compareRevision,
+ getState(drive) {
+ return chromeLocal.getValue(STORAGE_KEY + drive.name);
+ },
+ setState(drive, state) {
+ return chromeLocal.setValue(STORAGE_KEY + drive.name, state);
+ },
+ });
+ }
+
function schedule(delay = SYNC_DELAY) {
chrome.alarms.create('syncNow', {
delayInMinutes: delay,
@@ -220,4 +231,8 @@ const sync = API.sync = (() => {
}
throw new Error(`unknown cloud name: ${name}`);
}
-})();
+
+ //#endregion
+
+ return sync;
+});
diff --git a/background/tab-manager.js b/background/tab-manager.js
index 5a341c1f..b366a87f 100644
--- a/background/tab-manager.js
+++ b/background/tab-manager.js
@@ -1,32 +1,23 @@
-/* global navigatorUtil */
-/* exported tabManager */
'use strict';
-const tabManager = (() => {
- const listeners = [];
+define(require => {
+ const navigatorUtil = require('./navigator-util');
+
+ const listeners = new Set();
const cache = new Map();
chrome.tabs.onRemoved.addListener(tabId => cache.delete(tabId));
chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed));
- navigatorUtil.onUrlChange(({tabId, frameId, url}) => {
- const oldUrl = !frameId && tabManager.get(tabId, 'url', frameId);
- tabManager.set(tabId, 'url', frameId, url);
- if (frameId) return;
- for (const fn of listeners) {
- try {
- fn({tabId, url, oldUrl});
- } catch (err) {
- console.error(err);
- }
- }
- });
+ navigatorUtil.onUrlChange(notify);
- return {
+ const tabManager = {
onUpdate(fn) {
- listeners.push(fn);
+ listeners.add(fn);
},
+
get(tabId, ...keys) {
return keys.reduce((meta, key) => meta && meta[key], cache.get(tabId));
},
+
/**
* number of keys is arbitrary, last arg is value, `undefined` will delete the last key from meta
* (tabId, 'foo', 123) will set tabId's meta to {foo: 123},
@@ -47,8 +38,24 @@ const tabManager = (() => {
meta[lastKey] = value;
}
},
+
list() {
return cache.keys();
},
};
-})();
+
+ function notify({tabId, frameId, url}) {
+ const oldUrl = !frameId && tabManager.get(tabId, 'url', frameId);
+ tabManager.set(tabId, 'url', frameId, url);
+ if (frameId) return;
+ for (const fn of listeners) {
+ try {
+ fn({tabId, url, oldUrl});
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+
+ return tabManager;
+});
diff --git a/background/token-manager.js b/background/token-manager.js
index a5738e0f..9988032f 100644
--- a/background/token-manager.js
+++ b/background/token-manager.js
@@ -1,113 +1,125 @@
-/* global chromeLocal webextLaunchWebAuthFlow FIREFOX */
-/* exported tokenManager */
'use strict';
-const tokenManager = (() => {
- const AUTH = {
- dropbox: {
- flow: 'token',
- clientId: 'zg52vphuapvpng9',
- authURL: 'https://www.dropbox.com/oauth2/authorize',
- tokenURL: 'https://api.dropboxapi.com/oauth2/token',
- revoke: token =>
- fetch('https://api.dropboxapi.com/2/auth/token/revoke', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${token}`,
- },
- }),
- },
- google: {
- flow: 'code',
- clientId: '283762574871-d4u58s4arra5jdan2gr00heasjlttt1e.apps.googleusercontent.com',
- clientSecret: 'J0nc5TlR_0V_ex9-sZk-5faf',
- authURL: 'https://accounts.google.com/o/oauth2/v2/auth',
- authQuery: {
- // NOTE: Google needs 'prompt' parameter to deliver multiple refresh
- // tokens for multiple machines.
- // https://stackoverflow.com/q/18519185
- access_type: 'offline',
- prompt: 'consent',
- },
- tokenURL: 'https://oauth2.googleapis.com/token',
- scopes: ['https://www.googleapis.com/auth/drive.appdata'],
- revoke: token => {
- const params = {token};
- return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
- },
- },
- onedrive: {
- flow: 'code',
- clientId: '3864ce03-867c-4ad8-9856-371a097d47b1',
- clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w',
- authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
- tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
- redirect_uri: FIREFOX ?
- 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' :
- 'https://' + location.hostname + '.chromiumapp.org/',
- scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
- },
- };
+define(require => {
+ const {FIREFOX} = require('/js/toolbox');
+ const {chromeLocal} = require('/js/storage-util');
+
+ const AUTH = createAuth();
const NETWORK_LATENCY = 30; // seconds
- return {getToken, revokeToken, getClientId, buildKeys};
+ let exports;
+ const {
- function getClientId(name) {
- return AUTH[name].clientId;
- }
+ buildKeys,
- function buildKeys(name) {
- const k = {
- TOKEN: `secure/token/${name}/token`,
- EXPIRE: `secure/token/${name}/expire`,
- REFRESH: `secure/token/${name}/refresh`,
- };
- k.LIST = Object.values(k);
- return k;
- }
+ } = exports = {
- function getToken(name, interactive) {
- const k = buildKeys(name);
- return chromeLocal.get(k.LIST)
- .then(obj => {
- if (!obj[k.TOKEN]) {
+ buildKeys(name) {
+ const k = {
+ TOKEN: `secure/token/${name}/token`,
+ EXPIRE: `secure/token/${name}/expire`,
+ REFRESH: `secure/token/${name}/refresh`,
+ };
+ k.LIST = Object.values(k);
+ return k;
+ },
+
+ getClientId(name) {
+ return AUTH[name].clientId;
+ },
+
+ getToken(name, interactive) {
+ const k = buildKeys(name);
+ return chromeLocal.get(k.LIST)
+ .then(obj => {
+ if (!obj[k.TOKEN]) {
+ return authUser(name, k, interactive);
+ }
+ if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
+ return obj[k.TOKEN];
+ }
+ if (obj[k.REFRESH]) {
+ return refreshToken(name, k, obj)
+ .catch(err => {
+ if (err.code === 401) {
+ return authUser(name, k, interactive);
+ }
+ throw err;
+ });
+ }
return authUser(name, k, interactive);
- }
- if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
- return obj[k.TOKEN];
- }
- if (obj[k.REFRESH]) {
- return refreshToken(name, k, obj)
- .catch(err => {
- if (err.code === 401) {
- return authUser(name, k, interactive);
- }
- throw err;
- });
- }
- return authUser(name, k, interactive);
- });
- }
+ });
+ },
- async function revokeToken(name) {
- const provider = AUTH[name];
- const k = buildKeys(name);
- if (provider.revoke) {
- try {
- const token = await chromeLocal.getValue(k.TOKEN);
- if (token) {
- await provider.revoke(token);
+ async revokeToken(name) {
+ const provider = AUTH[name];
+ const k = buildKeys(name);
+ if (provider.revoke) {
+ try {
+ const token = await chromeLocal.getValue(k.TOKEN);
+ if (token) {
+ await provider.revoke(token);
+ }
+ } catch (e) {
+ console.error(e);
}
- } catch (e) {
- console.error(e);
}
- }
- await chromeLocal.remove(k.LIST);
+ await chromeLocal.remove(k.LIST);
+ },
+ };
+
+ function createAuth() {
+ return {
+ dropbox: {
+ flow: 'token',
+ clientId: 'zg52vphuapvpng9',
+ authURL: 'https://www.dropbox.com/oauth2/authorize',
+ tokenURL: 'https://api.dropboxapi.com/oauth2/token',
+ revoke: token =>
+ fetch('https://api.dropboxapi.com/2/auth/token/revoke', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ },
+ }),
+ },
+ google: {
+ flow: 'code',
+ clientId: '283762574871-d4u58s4arra5jdan2gr00heasjlttt1e.apps.googleusercontent.com',
+ clientSecret: 'J0nc5TlR_0V_ex9-sZk-5faf',
+ authURL: 'https://accounts.google.com/o/oauth2/v2/auth',
+ authQuery: {
+ // NOTE: Google needs 'prompt' parameter to deliver multiple refresh
+ // tokens for multiple machines.
+ // https://stackoverflow.com/q/18519185
+ access_type: 'offline',
+ prompt: 'consent',
+ },
+ tokenURL: 'https://oauth2.googleapis.com/token',
+ scopes: ['https://www.googleapis.com/auth/drive.appdata'],
+ revoke: token => {
+ const params = {token};
+ return postQuery(
+ `https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
+ },
+ },
+ onedrive: {
+ flow: 'code',
+ clientId: '3864ce03-867c-4ad8-9856-371a097d47b1',
+ clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w',
+ authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
+ tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
+ redirect_uri: FIREFOX ?
+ 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' :
+ 'https://' + location.hostname + '.chromiumapp.org/',
+ scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
+ },
+ };
}
- function refreshToken(name, k, obj) {
+ async function refreshToken(name, k, obj) {
if (!obj[k.REFRESH]) {
- return Promise.reject(new Error('no refresh token'));
+ throw new Error('No refresh token');
}
const provider = AUTH[name];
const body = {
@@ -119,17 +131,17 @@ const tokenManager = (() => {
if (provider.clientSecret) {
body.client_secret = provider.clientSecret;
}
- return postQuery(provider.tokenURL, body)
- .then(result => {
- if (!result.refresh_token) {
- // reuse old refresh token
- result.refresh_token = obj[k.REFRESH];
- }
- return handleTokenResult(result, k);
- });
+ const result = await postQuery(provider.tokenURL, body);
+ if (!result.refresh_token) {
+ // reuse old refresh token
+ result.refresh_token = obj[k.REFRESH];
+ }
+ return handleTokenResult(result, k);
}
- function authUser(name, k, interactive = false) {
+ async function authUser(name, k, interactive = false) {
+ await require(['js!/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min']);
+ /* global webextLaunchWebAuthFlow */
const provider = AUTH[name];
const state = Math.random().toFixed(8).slice(2);
const query = {
@@ -145,52 +157,54 @@ const tokenManager = (() => {
Object.assign(query, provider.authQuery);
}
const url = `${provider.authURL}?${new URLSearchParams(query)}`;
- return webextLaunchWebAuthFlow({
+ const finalUrl = await webextLaunchWebAuthFlow({
url,
interactive,
redirect_uri: query.redirect_uri,
- })
- .then(url => {
- const params = new URLSearchParams(
- provider.flow === 'token' ?
- new URL(url).hash.slice(1) :
- new URL(url).search.slice(1)
- );
- if (params.get('state') !== state) {
- throw new Error(`unexpected state: ${params.get('state')}, expected: ${state}`);
- }
- if (provider.flow === 'token') {
- const obj = {};
- for (const [key, value] of params.entries()) {
- obj[key] = value;
- }
- return obj;
- }
- const code = params.get('code');
- const body = {
- code,
- grant_type: 'authorization_code',
- client_id: provider.clientId,
- redirect_uri: query.redirect_uri,
- };
- if (provider.clientSecret) {
- body.client_secret = provider.clientSecret;
- }
- return postQuery(provider.tokenURL, body);
- })
- .then(result => handleTokenResult(result, k));
+ });
+ const params = new URLSearchParams(
+ provider.flow === 'token' ?
+ new URL(finalUrl).hash.slice(1) :
+ new URL(finalUrl).search.slice(1)
+ );
+ if (params.get('state') !== state) {
+ throw new Error(`Unexpected state: ${params.get('state')}, expected: ${state}`);
+ }
+ let result;
+ if (provider.flow === 'token') {
+ const obj = {};
+ for (const [key, value] of params) {
+ obj[key] = value;
+ }
+ result = obj;
+ } else {
+ const code = params.get('code');
+ const body = {
+ code,
+ grant_type: 'authorization_code',
+ client_id: provider.clientId,
+ redirect_uri: query.redirect_uri,
+ };
+ if (provider.clientSecret) {
+ body.client_secret = provider.clientSecret;
+ }
+ result = await postQuery(provider.tokenURL, body);
+ }
+ return handleTokenResult(result, k);
}
- function handleTokenResult(result, k) {
- return chromeLocal.set({
+ async function handleTokenResult(result, k) {
+ await chromeLocal.set({
[k.TOKEN]: result.access_token,
- [k.EXPIRE]: result.expires_in ? Date.now() + (Number(result.expires_in) - NETWORK_LATENCY) * 1000 : undefined,
+ [k.EXPIRE]: result.expires_in
+ ? Date.now() + (result.expires_in - NETWORK_LATENCY) * 1000
+ : undefined,
[k.REFRESH]: result.refresh_token,
- })
- .then(() => result.access_token);
+ });
+ return result.access_token;
}
- function postQuery(url, body) {
+ async function postQuery(url, body) {
const options = {
method: 'POST',
headers: {
@@ -198,17 +212,15 @@ const tokenManager = (() => {
},
body: body ? new URLSearchParams(body) : null,
};
- return fetch(url, options)
- .then(r => {
- if (r.ok) {
- return r.json();
- }
- return r.text()
- .then(body => {
- const err = new Error(`failed to fetch (${r.status}): ${body}`);
- err.code = r.status;
- throw err;
- });
- });
+ const r = await fetch(url, options);
+ if (r.ok) {
+ return r.json();
+ }
+ const text = await r.text();
+ const err = new Error(`Failed to fetch (${r.status}): ${text}`);
+ err.code = r.status;
+ throw err;
}
-})();
+
+ return exports;
+});
diff --git a/background/update.js b/background/update.js
index 182a008d..e16831d6 100644
--- a/background/update.js
+++ b/background/update.js
@@ -1,20 +1,21 @@
-/* global
- API
- calcStyleDigest
- chromeLocal
- debounce
- download
- ignoreChromeError
- prefs
- semverCompare
- styleJSONseemsValid
- styleSectionsEqual
- usercss
-*/
'use strict';
-(() => {
- const STATES = /** @namespace UpdaterStates */{
+define(require => {
+ const {API} = require('/js/msg');
+ const {
+ debounce,
+ download,
+ ignoreChromeError,
+ } = require('/js/toolbox');
+ const {
+ calcStyleDigest,
+ styleJSONseemsValid,
+ styleSectionsEqual,
+ } = require('/js/sections-util');
+ const {chromeLocal} = require('/js/storage-util');
+ const prefs = require('/js/prefs');
+
+ const STATES = /** @namespace UpdaterStates */ {
UPDATED: 'updated',
SKIPPED: 'skipped',
UNREACHABLE: 'server unreachable',
@@ -28,6 +29,7 @@
ERROR_JSON: 'error: JSON is invalid',
ERROR_VERSION: 'error: version is older than installed style',
};
+
const ALARM_NAME = 'scheduledUpdate';
const MIN_INTERVAL_MS = 60e3;
const RETRY_ERRORS = [
@@ -39,173 +41,174 @@
let logQueue = [];
let logLastWriteTime = 0;
- API.updater = {
- checkAllStyles,
- checkStyle,
- getStates: () => STATES,
- };
-
chromeLocal.getValue('lastUpdateTime').then(val => {
lastUpdateTime = val || Date.now();
- prefs.subscribe('updateInterval', schedule, {now: true});
+ prefs.subscribe('updateInterval', schedule, {runNow: true});
chrome.alarms.onAlarm.addListener(onAlarm);
});
- async function checkAllStyles({
- save = true,
- ignoreDigest,
- observe,
- } = {}) {
- resetInterval();
- checkingAll = true;
- const port = observe && chrome.runtime.connect({name: 'updater'});
- const styles = (await API.styles.getAll())
- .filter(style => style.updateUrl);
- if (port) port.postMessage({count: styles.length});
- log('');
- log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
- await Promise.all(
- styles.map(style =>
- checkStyle({style, port, save, ignoreDigest})));
- if (port) port.postMessage({done: true});
- if (port) port.disconnect();
- log('');
- checkingAll = false;
- }
+ /** @type {StyleUpdater} */
+ const updater = /** @namespace StyleUpdater */ {
- /**
- * @param {{
- id?: number
- style?: StyleObj
- port?: chrome.runtime.Port
- save?: boolean = true
- ignoreDigest?: boolean
- }} opts
- * @returns {{
- style: StyleObj
- updated?: boolean
- error?: any
- STATES: UpdaterStates
- }}
-
- Original style digests are calculated in these cases:
- * style is installed or updated from server
- * non-usercss style is checked for an update and styleSectionsEqual considers it unchanged
-
- Update check proceeds in these cases:
- * style has the original digest and it's equal to the current digest
- * [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
- * [ignoreDigest: none/false] style doesn't yet have the original digest
- so we compare the code to the server code and if it's the same we save the digest,
- otherwise we skip the style and report MAYBE_EDITED status
-
- 'ignoreDigest' option is set on the second manual individual update check on the manage page.
- */
- async function checkStyle(opts) {
- const {
- id,
- style = await API.styles.get(id),
+ async checkAllStyles({
+ save = true,
ignoreDigest,
- port,
- save,
- } = opts;
- const ucd = style.usercssData;
- let res, state;
- try {
- await checkIfEdited();
- res = {
- style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
- updated: true,
- };
- state = STATES.UPDATED;
- } catch (err) {
- const error = err === 0 && STATES.UNREACHABLE ||
- err && err.message ||
- err;
- res = {error, style, STATES};
- state = `${STATES.SKIPPED} (${error})`;
- }
- log(`${state} #${style.id} ${style.customName || style.name}`);
- if (port) port.postMessage(res);
- return res;
+ observe,
+ } = {}) {
+ resetInterval();
+ checkingAll = true;
+ const port = observe && chrome.runtime.connect({name: 'updater'});
+ const styles = (await API.styles.getAll())
+ .filter(style => style.updateUrl);
+ if (port) port.postMessage({count: styles.length});
+ log('');
+ log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
+ await Promise.all(
+ styles.map(style =>
+ updater.checkStyle({style, port, save, ignoreDigest})));
+ if (port) port.postMessage({done: true});
+ if (port) port.disconnect();
+ log('');
+ checkingAll = false;
+ },
- async function checkIfEdited() {
- if (!ignoreDigest &&
- style.originalDigest &&
- style.originalDigest !== await calcStyleDigest(style)) {
- return Promise.reject(STATES.EDITED);
- }
- }
+ /**
+ * @param {{
+ id?: number
+ style?: StyleObj
+ port?: chrome.runtime.Port
+ save?: boolean = true
+ ignoreDigest?: boolean
+ }} opts
+ * @returns {{
+ style: StyleObj
+ updated?: boolean
+ error?: any
+ STATES: UpdaterStates
+ }}
- async function updateUSO() {
- const md5 = await tryDownload(style.md5Url);
- if (!md5 || md5.length !== 32) {
- return Promise.reject(STATES.ERROR_MD5);
- }
- if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
- return Promise.reject(STATES.SAME_MD5);
- }
- const json = await tryDownload(style.updateUrl, {responseType: 'json'});
- if (!styleJSONseemsValid(json)) {
- return Promise.reject(STATES.ERROR_JSON);
- }
- // USO may not provide a correctly updated originalMd5 (#555)
- json.originalMd5 = md5;
- return json;
- }
+ Original style digests are calculated in these cases:
+ * style is installed or updated from server
+ * non-usercss style is checked for an update and styleSectionsEqual considers it unchanged
- async function updateUsercss() {
- // TODO: when sourceCode is > 100kB use http range request(s) for version check
- const text = await tryDownload(style.updateUrl);
- const json = await usercss.buildMeta(text);
- const delta = semverCompare(json.usercssData.version, ucd.version);
- if (!delta && !ignoreDigest) {
- // re-install is invalid in a soft upgrade
- const sameCode = text === style.sourceCode;
- return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
- }
- if (delta < 0) {
- // downgrade is always invalid
- return Promise.reject(STATES.ERROR_VERSION);
- }
- return usercss.buildCode(json);
- }
+ Update check proceeds in these cases:
+ * style has the original digest and it's equal to the current digest
+ * [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
+ * [ignoreDigest: none/false] style doesn't yet have the original digest
+ so we compare the code to the server code and if it's the same we save the digest,
+ otherwise we skip the style and report MAYBE_EDITED status
- async function maybeSave(json) {
- json.id = style.id;
- json.updateDate = Date.now();
- // keep current state
- delete json.customName;
- delete json.enabled;
- const newStyle = Object.assign({}, style, json);
- // update digest even if save === false as there might be just a space added etc.
- if (!ucd && styleSectionsEqual(json, style)) {
- style.originalDigest = (await API.styles.install(newStyle)).originalDigest;
- return Promise.reject(STATES.SAME_CODE);
+ 'ignoreDigest' option is set on the second manual individual update check on the manage page.
+ */
+ async checkStyle(opts) {
+ const {
+ id,
+ style = await API.styles.get(id),
+ ignoreDigest,
+ port,
+ save,
+ } = opts;
+ const ucd = style.usercssData;
+ let res, state;
+ try {
+ await checkIfEdited();
+ res = {
+ style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
+ updated: true,
+ };
+ state = STATES.UPDATED;
+ } catch (err) {
+ const error = err === 0 && STATES.UNREACHABLE ||
+ err && err.message ||
+ err;
+ res = {error, style, STATES};
+ state = `${STATES.SKIPPED} (${error})`;
}
- if (!style.originalDigest && !ignoreDigest) {
- return Promise.reject(STATES.MAYBE_EDITED);
- }
- return !save ? newStyle :
- (ucd ? API.usercss : API.styles).install(newStyle);
- }
+ log(`${state} #${style.id} ${style.customName || style.name}`);
+ if (port) port.postMessage(res);
+ return res;
- async function tryDownload(url, params) {
- let {retryDelay = 1000} = opts;
- while (true) {
- try {
- return await download(url, params);
- } catch (code) {
- if (!RETRY_ERRORS.includes(code) ||
- retryDelay > MIN_INTERVAL_MS) {
- return Promise.reject(code);
- }
+ async function checkIfEdited() {
+ if (!ignoreDigest &&
+ style.originalDigest &&
+ style.originalDigest !== await calcStyleDigest(style)) {
+ return Promise.reject(STATES.EDITED);
}
- retryDelay *= 1.25;
- await new Promise(resolve => setTimeout(resolve, retryDelay));
}
- }
- }
+
+ async function updateUSO() {
+ const md5 = await tryDownload(style.md5Url);
+ if (!md5 || md5.length !== 32) {
+ return Promise.reject(STATES.ERROR_MD5);
+ }
+ if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
+ return Promise.reject(STATES.SAME_MD5);
+ }
+ const json = await tryDownload(style.updateUrl, {responseType: 'json'});
+ if (!styleJSONseemsValid(json)) {
+ return Promise.reject(STATES.ERROR_JSON);
+ }
+ // USO may not provide a correctly updated originalMd5 (#555)
+ json.originalMd5 = md5;
+ return json;
+ }
+
+ async function updateUsercss() {
+ // TODO: when sourceCode is > 100kB use http range request(s) for version check
+ const text = await tryDownload(style.updateUrl);
+ const json = await API.usercss.buildMeta({sourceCode: text});
+ await require(['js!/vendor/semver-bundle/semver']); /* global semverCompare */
+ const delta = semverCompare(json.usercssData.version, ucd.version);
+ if (!delta && !ignoreDigest) {
+ // re-install is invalid in a soft upgrade
+ const sameCode = text === style.sourceCode;
+ return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
+ }
+ if (delta < 0) {
+ // downgrade is always invalid
+ return Promise.reject(STATES.ERROR_VERSION);
+ }
+ return API.usercss.buildCode(json);
+ }
+
+ async function maybeSave(json) {
+ json.id = style.id;
+ json.updateDate = Date.now();
+ // keep current state
+ delete json.customName;
+ delete json.enabled;
+ const newStyle = Object.assign({}, style, json);
+ // update digest even if save === false as there might be just a space added etc.
+ if (!ucd && styleSectionsEqual(json, style)) {
+ style.originalDigest = (await API.styles.install(newStyle)).originalDigest;
+ return Promise.reject(STATES.SAME_CODE);
+ }
+ if (!style.originalDigest && !ignoreDigest) {
+ return Promise.reject(STATES.MAYBE_EDITED);
+ }
+ return !save ? newStyle :
+ (ucd ? API.usercss.install : API.styles.install)(newStyle);
+ }
+
+ async function tryDownload(url, params) {
+ let {retryDelay = 1000} = opts;
+ while (true) {
+ try {
+ return await download(url, params);
+ } catch (code) {
+ if (!RETRY_ERRORS.includes(code) ||
+ retryDelay > MIN_INTERVAL_MS) {
+ return Promise.reject(code);
+ }
+ }
+ retryDelay *= 1.25;
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
+ }
+ }
+ },
+
+ getStates: () => STATES,
+ };
function schedule() {
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
@@ -220,7 +223,7 @@
}
function onAlarm({name}) {
- if (name === ALARM_NAME) checkAllStyles();
+ if (name === ALARM_NAME) updater.checkAllStyles();
}
function resetInterval() {
@@ -253,4 +256,6 @@
logLastWriteTime = Date.now();
logQueue = [];
}
-})();
+
+ return updater;
+});
diff --git a/background/usercss-api-helper.js b/background/usercss-api-helper.js
index b6508896..8f489618 100644
--- a/background/usercss-api-helper.js
+++ b/background/usercss-api-helper.js
@@ -1,81 +1,161 @@
-/* global
- API
- deepCopy
- usercss
-*/
'use strict';
-API.usercss = {
+define(require => {
+ const {API} = require('/js/msg');
+ const {deepCopy, download} = require('/js/toolbox');
- async build({
- styleId,
- sourceCode,
- vars,
- checkDup,
- metaOnly,
- assignVars,
- }) {
- let style = await usercss.buildMeta(sourceCode);
- const dup = (checkDup || assignVars) &&
- await API.usercss.find(styleId ? {id: styleId} : style);
- if (!metaOnly) {
- if (vars || assignVars) {
- await usercss.assignVars(style, vars ? {usercssData: {vars}} : dup);
+ const GLOBAL_METAS = {
+ author: undefined,
+ description: undefined,
+ homepageURL: 'url',
+ updateURL: 'updateUrl',
+ name: undefined,
+ };
+ const ERR_ARGS_IS_LIST = [
+ 'missingMandatory',
+ 'missingChar',
+ ];
+
+ const usercss = /** @namespace UsercssHelper */ {
+
+ rxMETA: /\/\*!?\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i,
+
+ async assignVars(style, oldStyle) {
+ const vars = style.usercssData.vars;
+ const oldVars = oldStyle.usercssData.vars;
+ if (vars && oldVars) {
+ // The type of var might be changed during the update. Set value to null if the value is invalid.
+ for (const [key, v] of Object.entries(vars)) {
+ const old = oldVars[key] && oldVars[key].value;
+ if (old) v.value = old;
+ }
+ style.usercssData.vars = await API.worker.nullifyInvalidVars(vars);
}
- style = await usercss.buildCode(style);
- }
- return {style, dup};
- },
+ },
- async buildMeta(style) {
- if (style.usercssData) {
+ async build({
+ styleId,
+ sourceCode,
+ vars,
+ checkDup,
+ metaOnly,
+ assignVars,
+ initialUrl,
+ }) {
+ // downloading here while install-usercss page is loading to avoid the wait
+ if (initialUrl) sourceCode = await download(initialUrl);
+ const style = await usercss.buildMeta({sourceCode});
+ const dup = (checkDup || assignVars) &&
+ await usercss.find(styleId ? {id: styleId} : style);
+ if (!metaOnly) {
+ if (vars || assignVars) {
+ await usercss.assignVars(style, vars ? {usercssData: {vars}} : dup);
+ }
+ await usercss.buildCode(style);
+ }
+ return {style, dup};
+ },
+
+ async buildCode(style) {
+ const {sourceCode: code, usercssData: {vars, preprocessor}} = style;
+ const match = code.match(usercss.rxMETA);
+ const i = match.index;
+ const j = i + match[0].length;
+ const codeNoMeta = code.slice(0, i) + blankOut(code, i, j) + code.slice(j);
+ const {sections, errors} = await API.worker.compileUsercss(preprocessor, codeNoMeta, vars);
+ const recoverable = errors.every(e => e.recoverable);
+ if (!sections.length || !recoverable) {
+ throw !recoverable ? errors : 'Style does not contain any actual CSS to apply.';
+ }
+ style.sections = sections;
return style;
- }
- // allow sourceCode to be normalized
- const {sourceCode} = style;
- delete style.sourceCode;
- return Object.assign(await usercss.buildMeta(sourceCode), style);
- },
+ },
- async configVars(id, vars) {
- let style = deepCopy(await API.styles.get(id));
- style.usercssData.vars = vars;
- style = await usercss.buildCode(style);
- style = await API.styles.install(style, 'config');
- return style.usercssData.vars;
- },
-
- async editSave(style) {
- return API.styles.editSave(await API.usercss.parse(style));
- },
-
- async find(styleOrData) {
- if (styleOrData.id) {
- return API.styles.get(styleOrData.id);
- }
- const {name, namespace} = styleOrData.usercssData || styleOrData;
- for (const dup of await API.styles.getAll()) {
- const data = dup.usercssData;
- if (data &&
- data.name === name &&
- data.namespace === namespace) {
- return dup;
+ async buildMeta(style) {
+ if (style.usercssData) {
+ return style;
}
- }
- },
+ // remember normalized sourceCode
+ let code = style.sourceCode = style.sourceCode.replace(/\r\n?/g, '\n');
+ style = Object.assign({
+ enabled: true,
+ sections: [],
+ }, style);
+ const match = code.match(usercss.rxMETA);
+ if (!match) {
+ return Promise.reject(new Error('Could not find metadata.'));
+ }
+ try {
+ code = blankOut(code, 0, match.index) + match[0];
+ const {metadata} = await API.worker.parseUsercssMeta(code);
+ style.usercssData = metadata;
+ // https://github.com/openstyles/stylus/issues/560#issuecomment-440561196
+ for (const [key, value] of Object.entries(GLOBAL_METAS)) {
+ if (metadata[key] !== undefined) {
+ style[value || key] = metadata[key];
+ }
+ }
+ return style;
+ } catch (err) {
+ if (err.code) {
+ const args = ERR_ARGS_IS_LIST.includes(err.code)
+ ? err.args.map(e => e.length === 1 ? JSON.stringify(e) : e).join(', ')
+ : err.args;
+ const msg = chrome.i18n.getMessage(`meta_${err.code}`, args);
+ if (msg) err.message = msg;
+ }
+ return Promise.reject(err);
+ }
+ },
- async install(style) {
- return API.styles.install(await API.usercss.parse(style));
- },
+ async configVars(id, vars) {
+ let style = deepCopy(await API.styles.get(id));
+ style.usercssData.vars = vars;
+ await usercss.buildCode(style);
+ style = await API.styles.install(style, 'config');
+ return style.usercssData.vars;
+ },
- async parse(style) {
- style = await API.usercss.buildMeta(style);
- // preserve style.vars during update
- const dup = await API.usercss.find(style);
- if (dup) {
- style.id = dup.id;
- await usercss.assignVars(style, dup);
- }
- return usercss.buildCode(style);
- },
-};
+ async editSave(style) {
+ return API.styles.editSave(await usercss.parse(style));
+ },
+
+ async find(styleOrData) {
+ if (styleOrData.id) {
+ return API.styles.get(styleOrData.id);
+ }
+ const {name, namespace} = styleOrData.usercssData || styleOrData;
+ for (const dup of await API.styles.getAll()) {
+ const data = dup.usercssData;
+ if (data &&
+ data.name === name &&
+ data.namespace === namespace) {
+ return dup;
+ }
+ }
+ },
+
+ async install(style) {
+ return API.styles.install(await usercss.parse(style));
+ },
+
+ async parse(style) {
+ style = await usercss.buildMeta(style);
+ // preserve style.vars during update
+ const dup = await usercss.find(style);
+ if (dup) {
+ style.id = dup.id;
+ await usercss.assignVars(style, dup);
+ }
+ return usercss.buildCode(style);
+ },
+ };
+
+ /** Replaces everything with spaces to keep the original length,
+ * but preserves the line breaks to keep the original line/col relation */
+ function blankOut(str, start = 0, end = str.length) {
+ return str.slice(start, end).replace(/[^\r\n]/g, ' ');
+ }
+
+ return usercss;
+});
diff --git a/background/usercss-install-helper.js b/background/usercss-install-helper.js
index 099a0822..c4147a13 100644
--- a/background/usercss-install-helper.js
+++ b/background/usercss-install-helper.js
@@ -1,36 +1,24 @@
-/* global
- API
- download
- openURL
- tabManager
- URLS
-*/
'use strict';
-(() => {
+define(require => {
+ const {
+ URLS,
+ download,
+ openURL,
+ } = require('/js/toolbox');
+ const tabManager = require('./tab-manager');
+
const installCodeCache = {};
- const clearInstallCode = url => delete installCodeCache[url];
- /** Sites may be using custom types like text/stylus so this coarse filter only excludes html */
- const isContentTypeText = type => /^text\/(?!html)/i.test(type);
- // in Firefox we have to use a content script to read file://
- const fileLoader = !chrome.app && (
- async tabId =>
- (await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0]);
+ const exports = /** @namespace UsercssHelper */ {
- const urlLoader =
- async (tabId, url) => (
- url.startsWith('file:') ||
- tabManager.get(tabId, isContentTypeText.name) ||
- isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
- ) && download(url);
-
- API.usercss.getInstallCode = url => {
- // when the installer tab is reloaded after the cache is expired, this will throw intentionally
- const {code, timer} = installCodeCache[url];
- clearInstallCode(url);
- clearTimeout(timer);
- return code;
+ getInstallCode(url) {
+ // when the installer tab is reloaded after the cache is expired, this will throw intentionally
+ const {code, timer} = installCodeCache[url];
+ clearInstallCode(url);
+ clearTimeout(timer);
+ return code;
+ },
};
// `glob`: pathname match pattern for webRequest
@@ -48,17 +36,7 @@
},
};
- // Faster installation on known distribution sites to avoid flicker of css text
- chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => {
- const u = new URL(url);
- const m = maybeDistro[u.hostname];
- if (!m || m.rx.test(u.pathname)) {
- openInstallerPage(tabId, url, {});
- // Silently suppress navigation.
- // Don't redirect to the install URL as it'll flash the text!
- return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
- }
- }, {
+ chrome.webRequest.onBeforeSendHeaders.addListener(maybeInstallFromDistro, {
urls: [
URLS.usoArchiveRaw + 'usercss/*.user.css',
'*://greasyfork.org/scripts/*/code/*.user.css',
@@ -70,27 +48,63 @@
types: ['main_frame'],
}, ['blocking']);
- // Remember Content-Type to avoid re-fetching of the headers in urlLoader as it can be very slow
- chrome.webRequest.onHeadersReceived.addListener(({tabId, responseHeaders}) => {
- const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type');
- tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
- }, {
+ chrome.webRequest.onHeadersReceived.addListener(rememberContentType, {
urls: makeUsercssGlobs('*', '/*'),
types: ['main_frame'],
}, ['responseHeaders']);
- tabManager.onUpdate(async ({tabId, url, oldUrl = ''}) => {
+ tabManager.onUpdate(maybeInstall);
+
+ function clearInstallCode(url) {
+ return delete installCodeCache[url];
+ }
+
+ /** Sites may be using custom types like text/stylus so this coarse filter only excludes html */
+ function isContentTypeText(type) {
+ return /^text\/(?!html)/i.test(type);
+ }
+
+ // in Firefox we have to use a content script to read file://
+ async function loadFromFile(tabId) {
+ return (await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0];
+ }
+
+ async function loadFromUrl(tabId, url) {
+ return (
+ url.startsWith('file:') ||
+ tabManager.get(tabId, isContentTypeText.name) ||
+ isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
+ ) && download(url);
+ }
+
+ function makeUsercssGlobs(host, path) {
+ return '%css,%css?*,%styl,%styl?*'.replace(/%/g, `*://${host}${path}.user.`).split(',');
+ }
+
+ async function maybeInstall({tabId, url, oldUrl = ''}) {
if (url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
!oldUrl.startsWith(URLS.installUsercss)) {
- const inTab = url.startsWith('file:') && Boolean(fileLoader);
- const code = await (inTab ? fileLoader : urlLoader)(tabId, url);
+ const inTab = url.startsWith('file:') && !chrome.app;
+ const code = await (inTab ? loadFromFile : loadFromUrl)(tabId, url);
if (/==userstyle==/i.test(code) && !/^\s* h.name.toLowerCase() === 'content-type');
+ tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
}
-})();
+
+ return exports;
+});
diff --git a/content/apply.js b/content/apply.js
index b4905b38..de80bb2b 100644
--- a/content/apply.js
+++ b/content/apply.js
@@ -1,18 +1,14 @@
-/* global msg API prefs createStyleInjector */
'use strict';
-// Chrome reruns content script when documentElement is replaced.
-// Note, we're checking against a literal `1`, not just `if (truthy)`,
-// because is exposed per HTML spec as a global variable and `window.INJECTED`.
-
-// eslint-disable-next-line no-unused-expressions
-self.INJECTED !== 1 && (() => {
- self.INJECTED = 1;
+define(require => {
+ const {API, msg} = require('/js/msg');
+ const prefs = require('/js/prefs');
let IS_TAB = !chrome.tabs || location.pathname !== '/popup.html';
const IS_FRAME = window !== parent;
const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument;
- const styleInjector = createStyleInjector({
+ /** @type {StyleInjector} */
+ const styleInjector = require('/content/style-injector')({
compare: (a, b) => a.id - b.id,
onUpdate: onInjectorUpdate,
});
@@ -210,4 +206,4 @@ self.INJECTED !== 1 && (() => {
msg.off(applyOnMessage);
} catch (e) {}
}
-})();
+});
diff --git a/content/install-hook-greasyfork.js b/content/install-hook-greasyfork.js
index 9de4cab3..e9a4293a 100644
--- a/content/install-hook-greasyfork.js
+++ b/content/install-hook-greasyfork.js
@@ -1,21 +1,14 @@
-/* global API */
'use strict';
-// onCommitted may fire twice
-// Note, we're checking against a literal `1`, not just `if (truthy)`,
-// because is exposed per HTML spec as a global variable and `window.INJECTED`.
-
-if (window.INJECTED_GREASYFORK !== 1) {
- window.INJECTED_GREASYFORK = 1;
- addEventListener('message', async function onMessage(e) {
- if (e.origin === location.origin &&
- e.data &&
- e.data.name &&
- e.data.type === 'style-version-query') {
- removeEventListener('message', onMessage);
- const style = await API.usercss.find(e.data) || {};
- const {version} = style.usercssData || {};
- postMessage({type: 'style-version', version}, '*');
- }
- });
-}
+addEventListener('message', async function onMessage(e) {
+ if (e.origin === location.origin &&
+ e.data &&
+ e.data.name &&
+ e.data.type === 'style-version-query') {
+ removeEventListener('message', onMessage);
+ const {API} = self.require('/js/msg');
+ const style = await API.usercss.find(e.data) || {};
+ const {version} = style.usercssData || {};
+ postMessage({type: 'style-version', version}, '*');
+ }
+});
diff --git a/content/install-hook-openusercss.js b/content/install-hook-openusercss.js
index e57da66d..edec2150 100644
--- a/content/install-hook-openusercss.js
+++ b/content/install-hook-openusercss.js
@@ -1,7 +1,8 @@
-/* global API */
'use strict';
-(() => {
+define(require => {
+ const {API} = require('/js/msg');
+
const manifest = chrome.runtime.getManifest();
const allowedOrigins = [
'https://openusercss.org',
@@ -55,7 +56,7 @@
window.addEventListener('message', installedHandler);
};
- const doHandshake = () => {
+ const doHandshake = event => {
// This is a representation of features that Stylus is capable of
const implementedFeatures = [
'install-usercss',
@@ -106,7 +107,7 @@
&& event.data.type === 'ouc-handshake-question'
&& allowedOrigins.includes(event.origin)
) {
- doHandshake();
+ doHandshake(event);
}
};
@@ -171,4 +172,4 @@
attachInstallListeners();
attachInstalledListeners();
askHandshake();
-})();
+});
diff --git a/content/install-hook-userstyles.js b/content/install-hook-userstyles.js
index f9330f43..dbdc1980 100644
--- a/content/install-hook-userstyles.js
+++ b/content/install-hook-userstyles.js
@@ -1,8 +1,10 @@
-/* global cloneInto msg API */
'use strict';
// eslint-disable-next-line no-unused-expressions
-/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
+/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) &&
+define(require => {
+ const {API, msg} = require('/js/msg');
+
const styleId = RegExp.$1;
const pageEventId = `${performance.now()}${Math.random()}`;
@@ -119,7 +121,7 @@
if (typeof cloneInto !== 'undefined') {
// Firefox requires explicit cloning, however USO can't process our messages anyway
// because USO tries to use a global "event" variable deprecated in Firefox
- detail = cloneInto({detail}, document);
+ detail = cloneInto({detail}, document); /* global cloneInto */
} else {
detail = {detail};
}
@@ -325,7 +327,7 @@
msg.off(onMessage);
} catch (e) {}
}
-})();
+});
function inPageContext(eventId) {
document.currentScript.remove();
diff --git a/content/style-injector.js b/content/style-injector.js
index 7a3fb0f2..e046e675 100644
--- a/content/style-injector.js
+++ b/content/style-injector.js
@@ -1,6 +1,10 @@
'use strict';
-self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
+/** The name is needed when running in content scripts but specifying it in define()
+ breaks IDE detection of exports so here's a workaround */
+define.currentModule = '/content/style-injector';
+
+define(require => ({
compare,
onUpdate = () => {},
}) => {
@@ -17,22 +21,22 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
// will store the original method refs because the page can override them
let creationDoc, createElement, createElementNS;
- return {
+ return /** @namespace StyleInjector */ {
list,
- apply(styleMap) {
+ async apply(styleMap) {
const styles = _styleMapToArray(styleMap);
- return (
- !styles.length ?
- Promise.resolve([]) :
- docRootObserver.evade(() => {
- if (!isTransitionPatched && isEnabled) {
- _applyTransitionPatch(styles);
- }
- return styles.map(_addUpdate);
- })
- ).then(_emitUpdate);
+ const value = !styles.length
+ ? []
+ : await docRootObserver.evade(() => {
+ if (!isTransitionPatched && isEnabled) {
+ _applyTransitionPatch(styles);
+ }
+ return styles.map(_addUpdate);
+ });
+ _emitUpdate();
+ return value;
},
clear() {
@@ -155,10 +159,9 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
docRootObserver[onOff]();
}
- function _emitUpdate(value) {
+ function _emitUpdate() {
_toggleObservers(list.length);
onUpdate();
- return value;
}
/*
@@ -321,4 +324,4 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
.observe(document, {childList: true});
}
}
-};
+});
diff --git a/edit.html b/edit.html
index e5bc1fdb..34889910 100644
--- a/edit.html
+++ b/edit.html
@@ -4,8 +4,6 @@
-
-
-
-
+
-
-
+
+
+
+
+
diff --git a/options/options.js b/options/options.js
index 6a9f9127..ae6d2c40 100644
--- a/options/options.js
+++ b/options/options.js
@@ -1,326 +1,340 @@
-/* global
- $
- $$
- $create
- $createLink
- API
- capitalize
- CHROME
- CHROME_HAS_BORDER_BUG
- enforceInputRange
- FIREFOX
- getEventKeyName
- ignoreChromeError
- messageBox
- msg
- openURL
- OPERA
- prefs
- setupLivePrefs
- t
- URLS
-*/
'use strict';
-setupLivePrefs();
-setupRadioButtons();
-$$('input[min], input[max]').forEach(enforceInputRange);
-setTimeout(splitLongTooltips);
+define(require => {
+ const {API, msg} = require('/js/msg');
+ const {
+ CHROME,
+ CHROME_HAS_BORDER_BUG,
+ OPERA,
+ FIREFOX,
+ URLS,
+ capitalize,
+ ignoreChromeError,
+ openURL,
+ } = require('/js/toolbox');
+ const t = require('/js/localization');
+ const {
+ $,
+ $$,
+ $create,
+ $createLink,
+ getEventKeyName,
+ messageBoxProxy,
+ setupLivePrefs,
+ } = require('/js/dom');
+ const prefs = require('/js/prefs');
-if (CHROME_HAS_BORDER_BUG) {
- const borderOption = $('.chrome-no-popup-border');
- if (borderOption) {
- borderOption.classList.remove('chrome-no-popup-border');
- }
-}
+ setupLivePrefs();
+ setupRadioButtons();
+ $$('input[min], input[max]').forEach(enforceInputRange);
+ setTimeout(splitLongTooltips);
-// collapse #advanced block in Chrome pre-66 (classic chrome://extensions UI)
-if (!FIREFOX && !OPERA && CHROME < 66) {
- const block = $('#advanced');
- $('h1', block).onclick = event => {
- event.preventDefault();
- block.classList.toggle('collapsed');
- const isCollapsed = block.classList.contains('collapsed');
- const visibleToggle = $(isCollapsed ? '.is-collapsed' : '.is-expanded', block);
- visibleToggle.focus();
- };
- block.classList.add('collapsible', 'collapsed');
-}
-
-if (FIREFOX && 'update' in (chrome.commands || {})) {
- $('[data-cmd="open-keyboard"]').classList.remove('chromium-only');
- msg.onExtension(msg => {
- if (msg.method === 'optionsCustomizeHotkeys') {
- customizeHotkeys();
- }
- });
-}
-
-if (CHROME && !chrome.declarativeContent) {
- // Show the option as disabled until the permission is actually granted
- const el = $('#styleViaXhr');
- prefs.initializing.then(() => {
- el.checked = false;
- });
- el.on('click', () => {
- if (el.checked) {
- chrome.permissions.request({permissions: ['declarativeContent']}, ignoreChromeError);
- }
- });
-}
-
-// actions
-$('#options-close-icon').onclick = () => {
- top.dispatchEvent(new CustomEvent('closeOptions'));
-};
-
-document.onclick = e => {
- const target = e.target.closest('[data-cmd]');
- if (!target) {
- return;
- }
- // prevent double-triggering in case a sub-element was clicked
- e.stopPropagation();
-
- switch (target.dataset.cmd) {
- case 'open-manage':
- API.openManage();
- break;
-
- case 'check-updates':
- checkUpdates();
- break;
-
- case 'open-keyboard':
- if (FIREFOX) {
- customizeHotkeys();
- } else {
- openURL({url: URLS.configureCommands});
- }
- e.preventDefault();
- break;
-
- case 'reset':
- $$('input')
- .filter(input => input.id in prefs.defaults)
- .forEach(input => prefs.reset(input.id));
- break;
-
- case 'note': {
- e.preventDefault();
- messageBox({
- className: 'note',
- contents: target.dataset.title,
- buttons: [t('confirmClose')],
- });
+ if (CHROME_HAS_BORDER_BUG) {
+ const borderOption = $('.chrome-no-popup-border');
+ if (borderOption) {
+ borderOption.classList.remove('chrome-no-popup-border');
}
}
-};
-// sync to cloud
-(() => {
- const elCloud = $('.sync-options .cloud-name');
- const elStart = $('.sync-options .connect');
- const elStop = $('.sync-options .disconnect');
- const elSyncNow = $('.sync-options .sync-now');
- const elStatus = $('.sync-options .sync-status');
- const elLogin = $('.sync-options .sync-login');
- /** @type {API.sync.Status} */
- let status = {};
- msg.onExtension(e => {
- if (e.method === 'syncStatusUpdate') {
- setStatus(e.status);
- }
- });
- API.sync.getStatus()
- .then(setStatus);
+ // collapse #advanced block in Chrome pre-66 (classic chrome://extensions UI)
+ if (!FIREFOX && !OPERA && CHROME < 66) {
+ const block = $('#advanced');
+ $('h1', block).onclick = event => {
+ event.preventDefault();
+ block.classList.toggle('collapsed');
+ const isCollapsed = block.classList.contains('collapsed');
+ const visibleToggle = $(isCollapsed ? '.is-collapsed' : '.is-expanded', block);
+ visibleToggle.focus();
+ };
+ block.classList.add('collapsible', 'collapsed');
+ }
- elCloud.on('change', updateButtons);
- for (const [btn, fn] of [
- [elStart, () => API.sync.start(elCloud.value)],
- [elStop, API.sync.stop],
- [elSyncNow, API.sync.syncNow],
- [elLogin, API.sync.login],
- ]) {
- btn.on('click', e => {
- if (getEventKeyName(e) === 'L') {
- fn();
+ if (FIREFOX && 'update' in (chrome.commands || {})) {
+ $('[data-cmd="open-keyboard"]').classList.remove('chromium-only');
+ }
+
+ if (CHROME && !chrome.declarativeContent) {
+ // Show the option as disabled until the permission is actually granted
+ const el = $('#styleViaXhr');
+ prefs.initializing.then(() => {
+ el.checked = false;
+ });
+ el.on('click', () => {
+ if (el.checked) {
+ chrome.permissions.request({permissions: ['declarativeContent']}, ignoreChromeError);
}
});
}
- function setStatus(newStatus) {
- status = newStatus;
- updateButtons();
- }
-
- function updateButtons() {
- const isConnected = status.state === 'connected';
- const isDisconnected = status.state === 'disconnected';
- if (status.currentDriveName) {
- elCloud.value = status.currentDriveName;
- }
- for (const [el, enable] of [
- [elCloud, isDisconnected],
- [elStart, isDisconnected && elCloud.value !== 'none'],
- [elStop, isConnected && !status.syncing],
- [elSyncNow, isConnected && !status.syncing],
- ]) {
- el.disabled = !enable;
- }
- elStatus.textContent = getStatusText();
- elLogin.hidden = !isConnected || status.login;
- }
-
- function getStatusText() {
- // chrome.i18n.getMessage is used instead of t() because calculated ids may be absent
- let res;
- if (status.syncing) {
- const {phase, loaded, total} = status.progress || {};
- res = phase
- ? chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) ||
- `${phase} ${loaded} / ${total}`
- : t('optionsSyncStatusSyncing');
- } else {
- const {state, errorMessage} = status;
- res = (state === 'connected' || state === 'disconnected') && errorMessage ||
- chrome.i18n.getMessage(`optionsSyncStatus${capitalize(state)}`) || state;
- }
- return res;
- }
-})();
-
-function checkUpdates() {
- let total = 0;
- let checked = 0;
- let updated = 0;
- const maxWidth = $('#update-progress').parentElement.clientWidth;
-
- chrome.runtime.onConnect.addListener(function onConnect(port) {
- if (port.name !== 'updater') return;
- port.onMessage.addListener(observer);
- chrome.runtime.onConnect.removeListener(onConnect);
- });
-
- API.updater.checkAllStyles({observe: true});
-
- function observer(info) {
- if ('count' in info) {
- total = info.count;
- document.body.classList.add('update-in-progress');
- } else if (info.updated) {
- updated++;
- checked++;
- } else if (info.error) {
- checked++;
- } else if (info.done) {
- document.body.classList.remove('update-in-progress');
- }
- $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
- $('#updates-installed').dataset.value = updated || '';
- }
-}
-
-function setupRadioButtons() {
- const sets = {};
- const onChange = function () {
- const newValue = sets[this.name].indexOf(this);
- if (newValue >= 0 && prefs.get(this.name) !== newValue) {
- prefs.set(this.name, newValue);
- }
+ // actions
+ $('#options-close-icon').onclick = () => {
+ top.dispatchEvent(new CustomEvent('closeOptions'));
};
- // group all radio-inputs by name="prefName" attribute
- for (const el of $$('input[type="radio"][name]')) {
- (sets[el.name] = sets[el.name] || []).push(el);
- el.on('change', onChange);
- }
- // select the input corresponding to the actual pref value
- for (const name in sets) {
- sets[name][prefs.get(name)].checked = true;
- }
- // listen to pref changes and update the values
- prefs.subscribe(Object.keys(sets), (key, value) => {
- sets[key][value].checked = true;
- });
-}
-function splitLongTooltips() {
- for (const el of $$('[title]')) {
- el.dataset.title = el.title;
- el.title = el.title.replace(/<\/?\w+>/g, ''); // strip html tags
- if (el.title.length < 50) {
- continue;
- }
- const newTitle = el.title
- .split('\n')
- .map(s => s.replace(/([^.][.。?!]|.{50,60},)\s+/g, '$1\n'))
- .map(s => s.replace(/(.{50,80}(?=.{40,}))\s+/g, '$1\n'))
- .join('\n');
- if (newTitle !== el.title) el.title = newTitle;
- }
-}
-
-function customizeHotkeys() {
- // command name -> i18n id
- const hotkeys = new Map([
- ['_execute_browser_action', 'optionsCustomizePopup'],
- ['openManage', 'openManage'],
- ['styleDisableAll', 'disableAllStyles'],
- ]);
-
- messageBox({
- title: t('shortcutsNote'),
- contents: [
- $create('table',
- [...hotkeys.entries()].map(([cmd, i18n]) =>
- $create('tr', [
- $create('td', t(i18n)),
- $create('td',
- $create('input', {
- id: 'hotkey.' + cmd,
- type: 'search',
- //placeholder: t('helpKeyMapHotkey'),
- })),
- ]))),
- ],
- className: 'center',
- buttons: [t('confirmClose')],
- onshow(box) {
- const ids = [];
- for (const cmd of hotkeys.keys()) {
- const id = 'hotkey.' + cmd;
- ids.push(id);
- $('#' + id).oninput = onInput;
- }
- setupLivePrefs(ids);
- $('button', box).insertAdjacentElement('beforebegin',
- $createLink(
- 'https://developer.mozilla.org/Add-ons/WebExtensions/manifest.json/commands#Key_combinations',
- t('helpAlt')));
- },
- });
-
- function onInput() {
- const name = this.id.split('.')[1];
- const shortcut = this.value.trim();
- if (!shortcut) {
- browser.commands.reset(name).catch(ignoreChromeError);
- this.setCustomValidity('');
+ document.onclick = async e => {
+ const target = e.target.closest('[data-cmd]');
+ if (!target) {
return;
}
- try {
- browser.commands.update({name, shortcut}).then(
- () => this.setCustomValidity(''),
- err => this.setCustomValidity(err)
- );
- } catch (err) {
- this.setCustomValidity(err);
+ // prevent double-triggering in case a sub-element was clicked
+ e.stopPropagation();
+
+ switch (target.dataset.cmd) {
+ case 'open-manage':
+ API.openManage();
+ break;
+
+ case 'check-updates':
+ checkUpdates();
+ break;
+
+ case 'open-keyboard':
+ if (FIREFOX) {
+ customizeHotkeys();
+ } else {
+ openURL({url: URLS.configureCommands});
+ }
+ e.preventDefault();
+ break;
+
+ case 'reset':
+ $$('input')
+ .filter(input => input.id in prefs.defaults)
+ .forEach(input => prefs.reset(input.id));
+ break;
+
+ case 'note': {
+ e.preventDefault();
+ messageBoxProxy.show({
+ className: 'note',
+ contents: target.dataset.title,
+ buttons: [t('confirmClose')],
+ });
+ }
+ }
+ };
+
+ // sync to cloud
+ (() => {
+ const elCloud = $('.sync-options .cloud-name');
+ const elStart = $('.sync-options .connect');
+ const elStop = $('.sync-options .disconnect');
+ const elSyncNow = $('.sync-options .sync-now');
+ const elStatus = $('.sync-options .sync-status');
+ const elLogin = $('.sync-options .sync-login');
+ /** @type {Sync.Status} */
+ let status = {};
+ msg.onExtension(e => {
+ if (e.method === 'syncStatusUpdate') {
+ setStatus(e.status);
+ }
+ });
+ API.sync.getStatus()
+ .then(setStatus);
+
+ elCloud.on('change', updateButtons);
+ for (const [btn, fn] of [
+ [elStart, () => API.sync.start(elCloud.value)],
+ [elStop, API.sync.stop],
+ [elSyncNow, API.sync.syncNow],
+ [elLogin, API.sync.login],
+ ]) {
+ btn.on('click', e => {
+ if (getEventKeyName(e) === 'L') {
+ fn();
+ }
+ });
+ }
+
+ function setStatus(newStatus) {
+ status = newStatus;
+ updateButtons();
+ }
+
+ function updateButtons() {
+ const {state, STATES} = status;
+ const isConnected = state === STATES.connected;
+ const isDisconnected = state === STATES.disconnected;
+ if (status.currentDriveName) {
+ elCloud.value = status.currentDriveName;
+ }
+ for (const [el, enable] of [
+ [elCloud, isDisconnected],
+ [elStart, isDisconnected && elCloud.value !== 'none'],
+ [elStop, isConnected && !status.syncing],
+ [elSyncNow, isConnected && !status.syncing],
+ ]) {
+ el.disabled = !enable;
+ }
+ elStatus.textContent = getStatusText();
+ elLogin.hidden = !isConnected || status.login;
+ }
+
+ function getStatusText() {
+ let res;
+ if (status.syncing) {
+ const {phase, loaded, total} = status.progress || {};
+ res = phase
+ ? t(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total], false) ||
+ `${phase} ${loaded} / ${total}`
+ : t('optionsSyncStatusSyncing');
+ } else {
+ const {state, errorMessage, STATES} = status;
+ res = (state === STATES.connected || state === STATES.disconnected) && errorMessage ||
+ t(`optionsSyncStatus${capitalize(state)}`, null, false) || state;
+ }
+ return res;
+ }
+ })();
+
+ function checkUpdates() {
+ let total = 0;
+ let checked = 0;
+ let updated = 0;
+ const maxWidth = $('#update-progress').parentElement.clientWidth;
+
+ chrome.runtime.onConnect.addListener(function onConnect(port) {
+ if (port.name !== 'updater') return;
+ port.onMessage.addListener(observer);
+ chrome.runtime.onConnect.removeListener(onConnect);
+ });
+
+ API.updater.checkAllStyles({observe: true});
+
+ function observer(info) {
+ if ('count' in info) {
+ total = info.count;
+ document.body.classList.add('update-in-progress');
+ } else if (info.updated) {
+ updated++;
+ checked++;
+ } else if (info.error) {
+ checked++;
+ } else if (info.done) {
+ document.body.classList.remove('update-in-progress');
+ }
+ $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
+ $('#updates-installed').dataset.value = updated || '';
}
}
-}
-window.onkeydown = event => {
- if (event.key === 'Escape') {
- top.dispatchEvent(new CustomEvent('closeOptions'));
+ async function customizeHotkeys() {
+ // command name -> i18n id
+ const hotkeys = new Map([
+ ['_execute_browser_action', 'optionsCustomizePopup'],
+ ['openManage', 'openManage'],
+ ['styleDisableAll', 'disableAllStyles'],
+ ]);
+
+ messageBoxProxy.show({
+ title: t('shortcutsNote'),
+ contents: [
+ $create('table',
+ [...hotkeys.entries()].map(([cmd, i18n]) =>
+ $create('tr', [
+ $create('td', t(i18n)),
+ $create('td',
+ $create('input', {
+ id: 'hotkey.' + cmd,
+ type: 'search',
+ //placeholder: t('helpKeyMapHotkey'),
+ })),
+ ]))),
+ ],
+ className: 'center',
+ buttons: [t('confirmClose')],
+ onshow(box) {
+ const ids = [];
+ for (const cmd of hotkeys.keys()) {
+ const id = 'hotkey.' + cmd;
+ ids.push(id);
+ $('#' + id).oninput = onInput;
+ }
+ setupLivePrefs(ids);
+ $('button', box).insertAdjacentElement('beforebegin',
+ $createLink(
+ 'https://developer.mozilla.org/Add-ons/WebExtensions/manifest.json/commands#Key_combinations',
+ t('helpAlt')));
+ },
+ });
+
+ function onInput() {
+ const name = this.id.split('.')[1];
+ const shortcut = this.value.trim();
+ if (!shortcut) {
+ browser.commands.reset(name).catch(ignoreChromeError);
+ this.setCustomValidity('');
+ return;
+ }
+ try {
+ browser.commands.update({name, shortcut}).then(
+ () => this.setCustomValidity(''),
+ err => this.setCustomValidity(err)
+ );
+ } catch (err) {
+ this.setCustomValidity(err);
+ }
+ }
}
-};
+
+ function enforceInputRange(element) {
+ const min = Number(element.min);
+ const max = Number(element.max);
+ const doNotify = () => element.dispatchEvent(new Event('change', {bubbles: true}));
+ const onChange = ({type}) => {
+ if (type === 'input' && element.checkValidity()) {
+ doNotify();
+ } else if (type === 'change' && !element.checkValidity()) {
+ element.value = Math.max(min, Math.min(max, Number(element.value)));
+ doNotify();
+ }
+ };
+ element.on('change', onChange);
+ element.on('input', onChange);
+ }
+
+ function splitLongTooltips() {
+ for (const el of $$('[title]')) {
+ el.dataset.title = el.title;
+ el.title = el.title.replace(/<\/?\w+>/g, ''); // strip html tags
+ if (el.title.length < 50) {
+ continue;
+ }
+ const newTitle = el.title
+ .split('\n')
+ .map(s => s.replace(/([^.][.。?!]|.{50,60},)\s+/g, '$1\n'))
+ .map(s => s.replace(/(.{50,80}(?=.{40,}))\s+/g, '$1\n'))
+ .join('\n');
+ if (newTitle !== el.title) el.title = newTitle;
+ }
+ }
+
+ function setupRadioButtons() {
+ const sets = {};
+ const onChange = function () {
+ const newValue = sets[this.name].indexOf(this);
+ if (newValue >= 0 && prefs.get(this.name) !== newValue) {
+ prefs.set(this.name, newValue);
+ }
+ };
+ // group all radio-inputs by name="prefName" attribute
+ for (const el of $$('input[type="radio"][name]')) {
+ (sets[el.name] = sets[el.name] || []).push(el);
+ el.on('change', onChange);
+ }
+ // select the input corresponding to the actual pref value
+ for (const name in sets) {
+ sets[name][prefs.get(name)].checked = true;
+ }
+ // listen to pref changes and update the values
+ prefs.subscribe(Object.keys(sets), (key, value) => {
+ sets[key][value].checked = true;
+ });
+ }
+
+ window.onkeydown = e => {
+ if (getEventKeyName(e) === 'Escape') {
+ top.dispatchEvent(new CustomEvent('closeOptions'));
+ }
+ };
+});
diff --git a/popup.html b/popup.html
index 5a894c16..dc9fa803 100644
--- a/popup.html
+++ b/popup.html
@@ -178,35 +178,24 @@
i18n-title="searchResultNotMatchingNote">
+
+
-
-
+
+
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
diff --git a/popup/events.js b/popup/events.js
new file mode 100644
index 00000000..9c4cb9d6
--- /dev/null
+++ b/popup/events.js
@@ -0,0 +1,226 @@
+'use strict';
+
+define(require => {
+ const {API} = require('/js/msg');
+ const {getActiveTab, tryJSONparse} = require('/js/toolbox');
+ const t = require('/js/localization');
+ const {
+ $,
+ $$,
+ $remove,
+ animateElement,
+ getEventKeyName,
+ moveFocus,
+ } = require('/js/dom');
+
+ const MODAL_SHOWN = 'data-display'; // attribute name
+
+ /** @type {PopupEvents} */
+ let exports;
+ const {
+
+ closeExplanation,
+ getClickedStyleElement,
+ getClickedStyleId,
+ getExcludeRule,
+ hideModal,
+ openURLandHide,
+ showModal,
+ thisTab,
+
+ } = exports = /** @namespace PopupEvents */ {
+
+ thisTab: {url: ''},
+
+ closeExplanation() {
+ $('#regexp-explanation').remove();
+ },
+
+ async configure(event) {
+ const {styleId, styleIsUsercss} = getClickedStyleElement(event);
+ if (styleIsUsercss) {
+ const style = await API.styles.get(styleId);
+ const hotkeys = await require(['./hotkeys']);
+ hotkeys.setState(false);
+ const configDialog = await require(['/js/dlg/config-dialog']);
+ await configDialog(style);
+ hotkeys.setState(true);
+ } else {
+ openURLandHide.call(this, event);
+ }
+ },
+
+ copyContent(event) {
+ event.preventDefault();
+ const target = document.activeElement;
+ const message = $('.copy-message');
+ navigator.clipboard.writeText(target.textContent);
+ target.classList.add('copied');
+ message.classList.add('show-message');
+ setTimeout(() => {
+ target.classList.remove('copied');
+ message.classList.remove('show-message');
+ }, 1000);
+ },
+
+ delete(event) {
+ const entry = getClickedStyleElement(event);
+ const box = $('#confirm');
+ box.dataset.id = entry.styleId;
+ $('b', box).textContent = $('.style-name', entry).textContent;
+ showModal(box, '[data-cmd=cancel]');
+ },
+
+ getClickedStyleId(event) {
+ return (getClickedStyleElement(event) || {}).styleId;
+ },
+
+ getClickedStyleElement(event) {
+ return event.target.closest('.entry');
+ },
+
+ getExcludeRule(type) {
+ const u = new URL(thisTab.url);
+ return type === 'domain'
+ ? u.origin + '/*'
+ : escapeGlob(u.origin + u.pathname); // current page
+ },
+
+ async hideModal(box, {animate} = {}) {
+ window.off('keydown', box._onkeydown);
+ box._onkeydown = null;
+ if (animate) {
+ box.style.animationName = '';
+ await animateElement(box, 'lights-on');
+ }
+ box.removeAttribute(MODAL_SHOWN);
+ },
+
+ indicator(event) {
+ const entry = getClickedStyleElement(event);
+ const info = t.template.regexpProblemExplanation.cloneNode(true);
+ $remove('#' + info.id);
+ $$('a', info).forEach(el => (el.onclick = openURLandHide));
+ $$('button', info).forEach(el => (el.onclick = closeExplanation));
+ entry.appendChild(info);
+ },
+
+ isStyleExcluded({exclusions}, type) {
+ if (!exclusions) {
+ return false;
+ }
+ const rule = getExcludeRule(type);
+ return exclusions.includes(rule);
+ },
+
+ maybeEdit(event) {
+ if (!(
+ event.button === 0 && (event.ctrlKey || event.metaKey) ||
+ event.button === 1 ||
+ event.button === 2)) {
+ return;
+ }
+ // open an editor on middleclick
+ const el = event.target;
+ if (el.matches('.entry, .style-edit-link') || el.closest('.style-name')) {
+ this.onmouseup = () => $('.style-edit-link', this).click();
+ this.oncontextmenu = event => event.preventDefault();
+ event.preventDefault();
+ return;
+ }
+ // prevent the popup being opened in a background tab
+ // when an irrelevant link was accidentally clicked
+ if (el.closest('a')) {
+ event.preventDefault();
+ return;
+ }
+ },
+
+ name(event) {
+ $('input', this).dispatchEvent(new MouseEvent('click'));
+ event.preventDefault();
+ },
+
+ async openEditor(event, options) {
+ event.preventDefault();
+ await API.openEditor(options);
+ window.close();
+ },
+
+ async openManager(event) {
+ event.preventDefault();
+ const isSearch = thisTab.url && (event.shiftKey || event.button === 2);
+ await API.openManage(isSearch ? {search: thisTab.url, searchMode: 'url'} : {});
+ window.close();
+ },
+
+ async openURLandHide(event) {
+ event.preventDefault();
+ await API.openURL({
+ url: this.href || this.dataset.href,
+ index: (await getActiveTab()).index + 1,
+ message: tryJSONparse(this.dataset.sendMessage),
+ });
+ window.close();
+ },
+
+ showModal(box, cancelButtonSelector) {
+ const oldBox = $(`[${MODAL_SHOWN}]`);
+ if (oldBox) box.style.animationName = 'none';
+ // '' would be fine but 'true' is backward-compatible with the existing userstyles
+ box.setAttribute(MODAL_SHOWN, 'true');
+ box._onkeydown = e => {
+ const key = getEventKeyName(e);
+ switch (key) {
+ case 'Tab':
+ case 'Shift-Tab':
+ e.preventDefault();
+ moveFocus(box, e.shiftKey ? -1 : 1);
+ break;
+ case 'Escape': {
+ e.preventDefault();
+ window.onkeydown = null;
+ $(cancelButtonSelector, box).click();
+ break;
+ }
+ }
+ };
+ window.on('keydown', box._onkeydown);
+ moveFocus(box, 0);
+ hideModal(oldBox);
+ },
+
+ async toggleState(event) {
+ // when fired on checkbox, prevent the parent label from seeing the event, see #501
+ event.stopPropagation();
+ await API.styles.toggle(getClickedStyleId(event), this.checked);
+ require(['./popup'], res => res.resortEntries());
+ },
+
+ toggleExclude(event, type) {
+ const entry = getClickedStyleElement(event);
+ if (event.target.checked) {
+ API.styles.addExclusion(entry.styleMeta.id, getExcludeRule(type));
+ } else {
+ API.styles.removeExclusion(entry.styleMeta.id, getExcludeRule(type));
+ }
+ },
+
+ toggleMenu(event) {
+ const entry = getClickedStyleElement(event);
+ const menu = $('.menu', entry);
+ if (menu.hasAttribute(MODAL_SHOWN)) {
+ hideModal(menu, {animate: true});
+ } else {
+ $('.menu-title', entry).textContent = $('.style-name', entry).textContent;
+ showModal(menu, '.menu-close');
+ }
+ },
+ };
+
+ function escapeGlob(text) {
+ return text.replace(/\*/g, '\\*');
+ }
+
+ return exports;
+});
diff --git a/popup/hotkeys.js b/popup/hotkeys.js
index 511f096c..690d2f55 100644
--- a/popup/hotkeys.js
+++ b/popup/hotkeys.js
@@ -1,35 +1,35 @@
-/* global $ $$ API debounce $create t */
'use strict';
-/* exported hotkeys */
-const hotkeys = (() => {
+define(require => {
+ const {API} = require('/js/msg');
+ const {debounce} = require('/js/toolbox');
+ const {$, $$, $create} = require('/js/dom');
+ const t = require('/js/localization');
+
const entries = document.getElementsByClassName('entry');
let togglablesShown;
let togglables;
let enabled = false;
let ready = false;
- window.addEventListener('showStyles:done', () => {
- togglablesShown = true;
- togglables = getTogglables();
- ready = true;
- setState(true);
- initHotkeyInfo();
- }, {once: true});
+ const hotkeys = {
- window.addEventListener('resize', adjustInfoPosition);
+ initHotkeys() {
+ togglablesShown = true;
+ togglables = getTogglables();
+ ready = true;
+ hotkeys.setState(true);
+ window.on('resize', adjustInfoPosition);
+ initHotkeyInfo();
+ },
- return {setState};
-
- function setState(newState = !enabled) {
- if (!ready) {
- throw new Error('hotkeys no ready');
- }
- if (newState !== enabled) {
- window[`${newState ? 'add' : 'remove'}EventListener`]('keydown', onKeyDown);
- enabled = newState;
- }
- }
+ setState(newState = !enabled) {
+ if (newState !== enabled && ready) {
+ window[newState ? 'on' : 'off']('keydown', onKeyDown);
+ enabled = newState;
+ }
+ },
+ };
function onKeyDown(event) {
if (event.ctrlKey || event.altKey || event.metaKey || !enabled ||
@@ -116,11 +116,11 @@ const hotkeys = (() => {
delete container.dataset.active;
document.body.style.height = '';
container.title = title;
- window.addEventListener('resize', adjustInfoPosition);
+ window.on('resize', adjustInfoPosition);
}
function open() {
- window.removeEventListener('resize', adjustInfoPosition);
+ window.off('resize', adjustInfoPosition);
debounce.unregister(adjustInfoPosition);
title = container.title;
container.title = '';
@@ -172,4 +172,6 @@ const hotkeys = (() => {
return;
}
}
-})();
+
+ return hotkeys;
+});
diff --git a/popup/popup-preinit.js b/popup/popup-preinit.js
deleted file mode 100644
index 3e3693e2..00000000
--- a/popup/popup-preinit.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/* global
- API
- URLS
-*/
-'use strict';
-
-const ABOUT_BLANK = 'about:blank';
-/* exported tabURL */
-/** @type string */
-let tabURL;
-
-/* exported initializing */
-const initializing = (async () => {
- let [tab] = await browser.tabs.query({currentWindow: true, active: true});
- if (!chrome.app && tab.status === 'loading' && tab.url === 'about:blank') {
- tab = await waitForTabUrlFF(tab);
- }
- const frames = sortTabFrames(await browser.webNavigation.getAllFrames({tabId: tab.id}));
- let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting
- if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) {
- url = frames[0].url || '';
- }
- if (!URLS.supported(url)) {
- url = '';
- frames.length = 1;
- }
- tabURL = frames[0].url = url;
- const uniqFrames = frames.filter(f => f.url && !f.isDupe);
- const styles = await Promise.all(uniqFrames.map(getFrameStyles));
- return {frames, styles};
-
- async function getFrameStyles({url}) {
- return {
- url,
- styles: await getStyleDataMerged(url),
- };
- }
-
- /** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
- function sortTabFrames(frames) {
- const unknown = new Map(frames.map(f => [f.frameId, f]));
- const known = new Map([[0, unknown.get(0) || {frameId: 0, url: ''}]]);
- unknown.delete(0);
- let lastSize = 0;
- while (unknown.size !== lastSize) {
- for (const [frameId, f] of unknown) {
- if (known.has(f.parentFrameId)) {
- unknown.delete(frameId);
- if (!f.errorOccurred) known.set(frameId, f);
- if (f.url === ABOUT_BLANK) f.url = known.get(f.parentFrameId).url;
- }
- }
- lastSize = unknown.size; // guard against an infinite loop due to a weird frame structure
- }
- const sortedFrames = [...known.values(), ...unknown.values()];
- const urls = new Set([ABOUT_BLANK]);
- for (const f of sortedFrames) {
- if (!f.url) f.url = '';
- f.isDupe = urls.has(f.url);
- urls.add(f.url);
- }
- return sortedFrames;
- }
-
- function waitForTabUrlFF(tab) {
- return new Promise(resolve => {
- browser.tabs.onUpdated.addListener(...[
- function onUpdated(tabId, info, updatedTab) {
- if (info.url && tabId === tab.id) {
- browser.tabs.onUpdated.removeListener(onUpdated);
- resolve(updatedTab);
- }
- },
- ...'UpdateFilter' in browser.tabs ? [{tabId: tab.id}] : [],
- // TODO: remove both spreads and tabId check when strict_min_version >= 61
- ]);
- });
- }
-})();
-
-/* Merges the extra props from API into style data.
- * When `id` is specified returns a single object otherwise an array */
-async function getStyleDataMerged(url, id) {
- const styles = (await API.styles.getByUrl(url, id))
- .map(r => Object.assign(r.style, r));
- return id ? styles[0] : styles;
-}
diff --git a/popup/popup.css b/popup/popup.css
index 40369b72..7936ee02 100644
--- a/popup/popup.css
+++ b/popup/popup.css
@@ -112,6 +112,10 @@ body > div:not(#installed):not(#message-box):not(.colorpicker-popup) {
cursor: pointer;
margin-right: .5em;
}
+#find-styles-inline-group label {
+ position: relative;
+ padding-left: 16px;
+}
.checker {
display: inline;
diff --git a/popup/popup.js b/popup/popup.js
index 1c5fe508..7d948364 100644
--- a/popup/popup.js
+++ b/popup/popup.js
@@ -1,657 +1,459 @@
-/* global
- $
- $$
- $create
- animateElement
- ABOUT_BLANK
- API
- CHROME
- CHROME_HAS_BORDER_BUG
- configDialog
- FIREFOX
- getActiveTab
- getEventKeyName
- getStyleDataMerged
- hotkeys
- initializing
- moveFocus
- msg
- onDOMready
- prefs
- setupLivePrefs
- t
- tabURL
- tryJSONparse
- URLS
-*/
-
'use strict';
-/** @type Element */
-let installed;
-const handleEvent = {};
+define(require => {
+ const {API, msg} = require('/js/msg');
+ const {isEmptyObj} = require('/js/polyfill');
+ const {
+ CHROME,
+ CHROME_HAS_BORDER_BUG,
+ FIREFOX,
+ URLS,
+ getActiveTab,
+ } = require('/js/toolbox');
+ const {
+ ABOUT_BLANK,
+ getStyleDataMerged,
+ initializing,
+ } = require('./preinit');
+ const t = require('/js/localization');
+ const {
+ $,
+ $$,
+ $create,
+ setupLivePrefs,
+ } = require('/js/dom');
+ const prefs = require('/js/prefs');
+ const Events = require('./events');
-const ENTRY_ID_PREFIX_RAW = 'style-';
-const MODAL_SHOWN = 'data-display'; // attribute name
+ /** @type Element */
+ let installed;
+ let tabURL;
+ const ENTRY_ID_PREFIX_RAW = 'style-';
-$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
-
-if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
- document.head.appendChild($create('style', 'html { overflow: overlay }'));
-}
-
-toggleSideBorders();
-
-Promise.all([
- initializing,
- onDOMready(),
-]).then(([
- {frames, styles},
-]) => {
- toggleUiSliders();
- initPopup(frames);
- if (styles[0]) {
- showStyles(styles);
- } else {
- // unsupported URL;
- $('#popup-manage-button').removeAttribute('title');
- }
-});
-
-msg.onExtension(onRuntimeMessage);
-
-prefs.subscribe(['popup.stylesFirst'], (key, stylesFirst) => {
- const actions = $('body > .actions');
- const before = stylesFirst ? actions : actions.nextSibling;
- document.body.insertBefore(installed, before);
-});
-prefs.subscribe(['popupWidth'], (key, value) => setPopupWidth(value));
-
-if (CHROME_HAS_BORDER_BUG) {
- prefs.subscribe(['popup.borders'], (key, value) => toggleSideBorders(value));
-}
-
-function onRuntimeMessage(msg) {
- if (!tabURL) return;
- let ready = Promise.resolve();
- switch (msg.method) {
- case 'styleAdded':
- case 'styleUpdated':
- if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
- ready = handleUpdate(msg);
- break;
- case 'styleDeleted':
- handleDelete(msg.style.id);
- break;
- }
- ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg})));
-}
-
-
-function setPopupWidth(width = prefs.get('popupWidth')) {
- document.body.style.width =
- Math.max(200, Math.min(800, width)) + 'px';
-}
-
-
-function toggleSideBorders(state = prefs.get('popup.borders')) {
- // runs before is parsed
- const style = document.documentElement.style;
- if (CHROME_HAS_BORDER_BUG && state) {
- style.cssText +=
- 'border-left: 2px solid white !important;' +
- 'border-right: 2px solid white !important;';
- } else if (style.cssText) {
- style.borderLeft = style.borderRight = '';
- }
-}
-
-function toggleUiSliders() {
- const sliders = prefs.get('ui.sliders');
- const slot = $('toggle', t.template.style);
- const toggle = t.template[sliders ? 'toggleSlider' : 'toggleChecker'];
- slot.parentElement.replaceChild(toggle.cloneNode(true), slot);
- document.body.classList.toggle('has-sliders', sliders);
-}
-
-/** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
-async function initPopup(frames) {
- installed = $('#installed');
-
- setPopupWidth();
-
- // action buttons
- $('#disableAll').onchange = function () {
- installed.classList.toggle('disabled', this.checked);
- };
- setupLivePrefs();
-
- Object.assign($('#popup-manage-button'), {
- onclick: handleEvent.openManager,
- oncontextmenu: handleEvent.openManager,
+ initializing.then(({frames, styles, url}) => {
+ tabURL = url;
+ Events.thisTab.url = url;
+ toggleUiSliders();
+ initPopup(frames);
+ if (styles[0]) {
+ showStyles(styles);
+ } else {
+ // unsupported URL;
+ $('#popup-manage-button').removeAttribute('title');
+ }
});
- $('#popup-options-button').onclick = () => {
- API.openManage({options: true});
- window.close();
- };
+ $.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
- $('#popup-wiki-button').onclick = handleEvent.openURLandHide;
+ msg.onExtension(onRuntimeMessage);
- $('#confirm').onclick = function (e) {
- const {id} = this.dataset;
- switch (e.target.dataset.cmd) {
- case 'ok':
- hideModal(this, {animate: true});
- API.styles.delete(Number(id));
+ prefs.subscribe('popup.stylesFirst', (key, stylesFirst) => {
+ const actions = $('body > .actions');
+ const before = stylesFirst ? actions : actions.nextSibling;
+ document.body.insertBefore(installed, before);
+ });
+ if (CHROME_HAS_BORDER_BUG) {
+ prefs.subscribe('popup.borders', toggleSideBorders, {runNow: true});
+ }
+ if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
+ document.head.appendChild($create('style', 'html { overflow: overlay }'));
+ }
+
+ function onRuntimeMessage(msg) {
+ if (!tabURL) return;
+ let ready = Promise.resolve();
+ switch (msg.method) {
+ case 'styleAdded':
+ case 'styleUpdated':
+ if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
+ ready = handleUpdate(msg);
break;
- case 'cancel':
- showModal($('.menu', $.entry(id)), '.menu-close');
+ case 'styleDeleted':
+ handleDelete(msg.style.id);
break;
}
- };
-
- if (!prefs.get('popup.stylesFirst')) {
- document.body.insertBefore(
- $('body > .actions'),
- installed);
+ ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg})));
}
- for (const el of $$('link[media=print]')) {
- el.removeAttribute('media');
+ function setPopupWidth(_key, width) {
+ document.body.style.width =
+ Math.max(200, Math.min(800, width)) + 'px';
}
- if (!tabURL) {
- blockPopup();
- return;
+ function toggleSideBorders(_key, state) {
+ // runs before is parsed
+ const style = document.documentElement.style;
+ if (state) {
+ style.cssText +=
+ 'border-left: 2px solid white !important;' +
+ 'border-right: 2px solid white !important;';
+ } else if (style.cssText) {
+ style.borderLeft = style.borderRight = '';
+ }
}
- frames.forEach(createWriterElement);
- if (frames.length > 1) {
- const el = $('#write-for-frames');
- el.hidden = false;
- el.onclick = () => el.classList.toggle('expanded');
+ function toggleUiSliders() {
+ const sliders = prefs.get('ui.sliders');
+ const slot = $('toggle', t.template.style);
+ const toggle = t.template[sliders ? 'toggleSlider' : 'toggleChecker'];
+ slot.parentElement.replaceChild(toggle.cloneNode(true), slot);
+ document.body.classList.toggle('has-sliders', sliders);
}
- const isStore = tabURL.startsWith(URLS.browserWebStore);
- if (isStore && !FIREFOX) {
- blockPopup();
- return;
- }
+ /** @param {chrome.webNavigation.GetAllFrameResultDetails[]} frames */
+ async function initPopup(frames) {
+ installed = $('#installed');
- for (let retryCountdown = 10; retryCountdown-- > 0;) {
- const tab = await getActiveTab();
- if (await msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0}).catch(() => {})) {
+ prefs.subscribe('popupWidth', setPopupWidth, {runNow: true});
+
+ // action buttons
+ $('#disableAll').onchange = function () {
+ installed.classList.toggle('disabled', this.checked);
+ };
+ setupLivePrefs();
+
+ Object.assign($('#find-styles-link'), {
+ href: URLS.usoArchive,
+ onclick(e) {
+ e.preventDefault();
+ require(['./search'], res => res.onclick.call(this, e));
+ },
+ });
+
+ Object.assign($('#popup-manage-button'), {
+ onclick: Events.openManager,
+ oncontextmenu: Events.openManager,
+ });
+
+ $('#popup-options-button').onclick = () => {
+ API.openManage({options: true});
+ window.close();
+ };
+
+ $('#popup-wiki-button').onclick = Events.openURLandHide;
+
+ $('#confirm').onclick = function (e) {
+ const {id} = this.dataset;
+ switch (e.target.dataset.cmd) {
+ case 'ok':
+ Events.hideModal(this, {animate: true});
+ API.styles.delete(Number(id));
+ break;
+ case 'cancel':
+ Events.showModal($('.menu', $.entry(id)), '.menu-close');
+ break;
+ }
+ };
+
+ if (!prefs.get('popup.stylesFirst')) {
+ document.body.insertBefore(
+ $('body > .actions'),
+ installed);
+ }
+
+ for (const el of $$('link[media=print]')) {
+ el.removeAttribute('media');
+ }
+
+ if (!tabURL) {
+ blockPopup();
return;
}
- if (tab.status === 'complete' && (!FIREFOX || tab.url !== ABOUT_BLANK)) {
- break;
+
+ frames.forEach(createWriterElement);
+ if (frames.length > 1) {
+ const el = $('#write-for-frames');
+ el.hidden = false;
+ el.onclick = () => el.classList.toggle('expanded');
}
- // FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand
- // so we'll wait a bit to handle popup being invoked right after switching
- await new Promise(resolve => setTimeout(resolve, 100));
- }
- initUnreachable(isStore);
-}
-
-function initUnreachable(isStore) {
- const info = t.template.unreachableInfo;
- if (!FIREFOX) {
- // Chrome "Allow access to file URLs" in chrome://extensions message
- info.appendChild($create('p', t('unreachableFileHint')));
- } else if (isStore) {
- $('label', info).textContent = t('unreachableAMO');
- const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) +
- (FIREFOX < 60 ? '' : '\n' + t('unreachableAMOHintNewFF'));
- const renderToken = s => s[0] === '<'
- ? $create('a', {
- textContent: s.slice(1, -1),
- onclick: handleEvent.copyContent,
- href: '#',
- className: 'copy',
- tabIndex: 0,
- title: t('copy'),
- })
- : s;
- const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken));
- const noteNode = $create('fragment', note.split('\n').map(renderLine));
- info.appendChild(noteNode);
- }
- // Inaccessible locally hosted file type, e.g. JSON, PDF, etc.
- if (tabURL.length - tabURL.lastIndexOf('.') <= 5) {
- info.appendChild($create('p', t('InaccessibleFileHint')));
- }
- document.body.classList.add('unreachable');
- document.body.insertBefore(info, document.body.firstChild);
-}
-
-/** @param {chrome.webNavigation.GetAllFrameResultDetails} frame */
-function createWriterElement(frame) {
- const {url, frameId, parentFrameId, isDupe} = frame;
- const targets = $create('span');
-
- // For this URL
- const urlLink = t.template.writeStyle.cloneNode(true);
- const isAboutBlank = url === ABOUT_BLANK;
- Object.assign(urlLink, {
- href: 'edit.html?url-prefix=' + encodeURIComponent(url),
- title: `url-prefix("${url}")`,
- tabIndex: isAboutBlank ? -1 : 0,
- textContent: prefs.get('popup.breadcrumbs.usePath')
- ? new URL(url).pathname.slice(1)
- : frameId
- ? isAboutBlank ? url : 'URL'
- : t('writeStyleForURL').replace(/ /g, '\u00a0'), // this URL
- onclick: e => handleEvent.openEditor(e, {'url-prefix': url}),
- });
- if (prefs.get('popup.breadcrumbs')) {
- urlLink.onmouseenter =
- urlLink.onfocus = () => urlLink.parentNode.classList.add('url()');
- urlLink.onmouseleave =
- urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
- }
- targets.appendChild(urlLink);
-
- // For domain
- const domains = getDomains(url);
- for (const domain of domains) {
- const numParts = domain.length - domain.replace(/\./g, '').length + 1;
- // Don't include TLD
- if (domains.length > 1 && numParts === 1) {
- continue;
+ const isStore = tabURL.startsWith(URLS.browserWebStore);
+ if (isStore && !FIREFOX) {
+ blockPopup();
+ return;
}
- const domainLink = t.template.writeStyle.cloneNode(true);
- Object.assign(domainLink, {
- href: 'edit.html?domain=' + encodeURIComponent(domain),
- textContent: numParts > 2 ? domain.split('.')[0] : domain,
- title: `domain("${domain}")`,
- onclick: e => handleEvent.openEditor(e, {domain}),
- });
- domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : '');
- targets.appendChild(domainLink);
- }
- if (prefs.get('popup.breadcrumbs')) {
- targets.classList.add('breadcrumbs');
- targets.appendChild(urlLink); // making it the last element
- }
-
- const root = $('#write-style');
- const parent = $(`[data-frame-id="${parentFrameId}"]`, root) || root;
- const child = $create({
- tag: 'span',
- className: `match${isDupe ? ' dupe' : ''}${isAboutBlank ? ' about-blank' : ''}`,
- dataset: {frameId},
- appendChild: targets,
- });
- parent.appendChild(child);
- parent.dataset.children = (Number(parent.dataset.children) || 0) + 1;
-}
-
-function getDomains(url) {
- let d = url.split(/[/:]+/, 2)[1];
- if (!d || url.startsWith('file:')) {
- return [];
- }
- const domains = [d];
- while (d.includes('.')) {
- d = d.substring(d.indexOf('.') + 1);
- domains.push(d);
- }
- return domains;
-}
-
-function sortStyles(entries) {
- const enabledFirst = prefs.get('popup.enabledFirst');
- return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
- Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
- enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
- (a.customName || a.name).localeCompare(b.customName || b.name));
-}
-
-function showStyles(frameResults) {
- const entries = new Map();
- frameResults.forEach(({styles = [], url}, index) => {
- styles.forEach(style => {
- const {id} = style;
- if (!entries.has(id)) {
- style.frameUrl = index === 0 ? '' : url;
- entries.set(id, createStyleElement(style));
+ for (let retryCountdown = 10; retryCountdown-- > 0;) {
+ const tab = await getActiveTab();
+ if (await msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0}).catch(() => {})) {
+ return;
}
- });
- });
- if (entries.size) {
- resortEntries([...entries.values()]);
- } else {
- installed.appendChild(t.template.noStyles);
+ if (tab.status === 'complete' && (!FIREFOX || tab.url !== ABOUT_BLANK)) {
+ break;
+ }
+ // FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand
+ // so we'll wait a bit to handle popup being invoked right after switching
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ initUnreachable(isStore);
}
- window.dispatchEvent(new Event('showStyles:done'));
-}
-function resortEntries(entries) {
- // `entries` is specified only at startup, after that we respect the prefs
- if (entries || prefs.get('popup.autoResort')) {
- installed.append(...sortStyles(entries || $$('.entry', installed)));
+ function initUnreachable(isStore) {
+ const info = t.template.unreachableInfo;
+ if (!FIREFOX) {
+ // Chrome "Allow access to file URLs" in chrome://extensions message
+ info.appendChild($create('p', t('unreachableFileHint')));
+ } else if (isStore) {
+ $('label', info).textContent = t('unreachableAMO');
+ const note = (FIREFOX < 59 ? t('unreachableAMOHintOldFF') : t('unreachableAMOHint')) +
+ (FIREFOX < 60 ? '' : '\n' + t('unreachableAMOHintNewFF'));
+ const renderToken = s => s[0] === '<'
+ ? $create('a', {
+ textContent: s.slice(1, -1),
+ onclick: Events.copyContent,
+ href: '#',
+ className: 'copy',
+ tabIndex: 0,
+ title: t('copy'),
+ })
+ : s;
+ const renderLine = line => $create('p', line.split(/(<.*?>)/).map(renderToken));
+ const noteNode = $create('fragment', note.split('\n').map(renderLine));
+ info.appendChild(noteNode);
+ }
+ // Inaccessible locally hosted file type, e.g. JSON, PDF, etc.
+ if (tabURL.length - tabURL.lastIndexOf('.') <= 5) {
+ info.appendChild($create('p', t('InaccessibleFileHint')));
+ }
+ document.body.classList.add('unreachable');
+ document.body.insertBefore(info, document.body.firstChild);
}
-}
-function createStyleElement(style) {
- let entry = $.entry(style);
- if (!entry) {
- entry = t.template.style.cloneNode(true);
- Object.assign(entry, {
- id: ENTRY_ID_PREFIX_RAW + style.id,
- styleId: style.id,
- styleIsUsercss: Boolean(style.usercssData),
- onmousedown: handleEvent.maybeEdit,
- styleMeta: style,
- });
- Object.assign($('input', entry), {
- onclick: handleEvent.toggle,
- });
- const editLink = $('.style-edit-link', entry);
- Object.assign(editLink, {
- href: editLink.getAttribute('href') + style.id,
- onclick: e => handleEvent.openEditor(e, {id: style.id}),
- });
- const styleName = $('.style-name', entry);
- Object.assign(styleName, {
- htmlFor: ENTRY_ID_PREFIX_RAW + style.id,
- onclick: handleEvent.name,
- });
- styleName.appendChild(document.createTextNode(' '));
+ /** @param {chrome.webNavigation.GetAllFrameResultDetails} frame */
+ function createWriterElement(frame) {
+ const {url, frameId, parentFrameId, isDupe} = frame;
+ const targets = $create('span');
- const config = $('.configure', entry);
- config.onclick = handleEvent.configure;
- if (!style.usercssData) {
- if (style.updateUrl && style.updateUrl.includes('?') && style.url) {
- config.href = style.url;
- config.target = '_blank';
- config.title = t('configureStyleOnHomepage');
- config.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
- $('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso';
- } else {
+ // For this URL
+ const urlLink = t.template.writeStyle.cloneNode(true);
+ const isAboutBlank = url === ABOUT_BLANK;
+ Object.assign(urlLink, {
+ href: 'edit.html?url-prefix=' + encodeURIComponent(url),
+ title: `url-prefix("${url}")`,
+ tabIndex: isAboutBlank ? -1 : 0,
+ textContent: prefs.get('popup.breadcrumbs.usePath')
+ ? new URL(url).pathname.slice(1)
+ : frameId
+ ? isAboutBlank ? url : 'URL'
+ : t('writeStyleForURL').replace(/ /g, '\u00a0'), // this URL
+ onclick: e => Events.openEditor(e, {'url-prefix': url}),
+ });
+ if (prefs.get('popup.breadcrumbs')) {
+ urlLink.onmouseenter =
+ urlLink.onfocus = () => urlLink.parentNode.classList.add('url()');
+ urlLink.onmouseleave =
+ urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
+ }
+ targets.appendChild(urlLink);
+
+ // For domain
+ const domains = getDomains(url);
+ for (const domain of domains) {
+ const numParts = domain.length - domain.replace(/\./g, '').length + 1;
+ // Don't include TLD
+ if (domains.length > 1 && numParts === 1) {
+ continue;
+ }
+ const domainLink = t.template.writeStyle.cloneNode(true);
+ Object.assign(domainLink, {
+ href: 'edit.html?domain=' + encodeURIComponent(domain),
+ textContent: numParts > 2 ? domain.split('.')[0] : domain,
+ title: `domain("${domain}")`,
+ onclick: e => Events.openEditor(e, {domain}),
+ });
+ domainLink.setAttribute('subdomain', numParts > 1 ? 'true' : '');
+ targets.appendChild(domainLink);
+ }
+
+ if (prefs.get('popup.breadcrumbs')) {
+ targets.classList.add('breadcrumbs');
+ targets.appendChild(urlLink); // making it the last element
+ }
+
+ const root = $('#write-style');
+ const parent = $(`[data-frame-id="${parentFrameId}"]`, root) || root;
+ const child = $create({
+ tag: 'span',
+ className: `match${isDupe ? ' dupe' : ''}${isAboutBlank ? ' about-blank' : ''}`,
+ dataset: {frameId},
+ appendChild: targets,
+ });
+ parent.appendChild(child);
+ parent.dataset.children = (Number(parent.dataset.children) || 0) + 1;
+ }
+
+ function getDomains(url) {
+ let d = url.split(/[/:]+/, 2)[1];
+ if (!d || url.startsWith('file:')) {
+ return [];
+ }
+ const domains = [d];
+ while (d.includes('.')) {
+ d = d.substring(d.indexOf('.') + 1);
+ domains.push(d);
+ }
+ return domains;
+ }
+
+ function sortStyles(entries) {
+ const enabledFirst = prefs.get('popup.enabledFirst');
+ return entries.sort(({styleMeta: a}, {styleMeta: b}) =>
+ Boolean(a.frameUrl) - Boolean(b.frameUrl) ||
+ enabledFirst && Boolean(b.enabled) - Boolean(a.enabled) ||
+ (a.customName || a.name).localeCompare(b.customName || b.name));
+ }
+
+ function showStyles(frameResults) {
+ const entries = new Map();
+ frameResults.forEach(({styles = [], url}, index) => {
+ styles.forEach(style => {
+ const {id} = style;
+ if (!entries.has(id)) {
+ style.frameUrl = index === 0 ? '' : url;
+ entries.set(id, createStyleElement(style));
+ }
+ });
+ });
+ if (entries.size) {
+ resortEntries([...entries.values()]);
+ } else {
+ installed.appendChild(t.template.noStyles);
+ }
+ require(['./hotkeys'], m => m.initHotkeys());
+ }
+
+ function resortEntries(entries) {
+ // `entries` is specified only at startup, after that we respect the prefs
+ if (entries || prefs.get('popup.autoResort')) {
+ installed.append(...sortStyles(entries || $$('.entry', installed)));
+ }
+ }
+
+ function createStyleElement(style) {
+ let entry = $.entry(style);
+ if (!entry) {
+ entry = t.template.style.cloneNode(true);
+ Object.assign(entry, {
+ id: ENTRY_ID_PREFIX_RAW + style.id,
+ styleId: style.id,
+ styleIsUsercss: Boolean(style.usercssData),
+ onmousedown: Events.maybeEdit,
+ styleMeta: style,
+ });
+ Object.assign($('input', entry), {
+ onclick: Events.toggleState,
+ });
+ const editLink = $('.style-edit-link', entry);
+ Object.assign(editLink, {
+ href: editLink.getAttribute('href') + style.id,
+ onclick: e => Events.openEditor(e, {id: style.id}),
+ });
+ const styleName = $('.style-name', entry);
+ Object.assign(styleName, {
+ htmlFor: ENTRY_ID_PREFIX_RAW + style.id,
+ onclick: Events.name,
+ });
+ styleName.appendChild(document.createTextNode(' '));
+
+ const config = $('.configure', entry);
+ config.onclick = Events.configure;
+ if (!style.usercssData) {
+ if (style.updateUrl && style.updateUrl.includes('?') && style.url) {
+ config.href = style.url;
+ config.target = '_blank';
+ config.title = t('configureStyleOnHomepage');
+ config.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
+ $('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso';
+ } else {
+ config.classList.add('hidden');
+ }
+ } else if (isEmptyObj(style.usercssData.vars)) {
config.classList.add('hidden');
}
- } else if (Object.keys(style.usercssData.vars || {}).length === 0) {
- config.classList.add('hidden');
+
+ $('.delete', entry).onclick = Events.delete;
+
+ const indicator = t.template.regexpProblemIndicator.cloneNode(true);
+ indicator.appendChild(document.createTextNode('!'));
+ indicator.onclick = Events.indicator;
+ $('.main-controls', entry).appendChild(indicator);
+
+ $('.menu-button', entry).onclick = Events.toggleMenu;
+ $('.menu-close', entry).onclick = Events.toggleMenu;
+
+ $('.exclude-by-domain-checkbox', entry).onchange = e => Events.toggleExclude(e, 'domain');
+ $('.exclude-by-url-checkbox', entry).onchange = e => Events.toggleExclude(e, 'url');
}
- $('.delete', entry).onclick = handleEvent.delete;
+ style = Object.assign(entry.styleMeta, style);
- const indicator = t.template.regexpProblemIndicator.cloneNode(true);
- indicator.appendChild(document.createTextNode('!'));
- indicator.onclick = handleEvent.indicator;
- $('.main-controls', entry).appendChild(indicator);
+ entry.classList.toggle('disabled', !style.enabled);
+ entry.classList.toggle('enabled', style.enabled);
+ $('input', entry).checked = style.enabled;
- $('.menu-button', entry).onclick = handleEvent.toggleMenu;
- $('.menu-close', entry).onclick = handleEvent.toggleMenu;
-
- $('.exclude-by-domain-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'domain');
- $('.exclude-by-url-checkbox', entry).onchange = e => handleEvent.toggleExclude(e, 'url');
- }
-
- style = Object.assign(entry.styleMeta, style);
-
- entry.classList.toggle('disabled', !style.enabled);
- entry.classList.toggle('enabled', style.enabled);
- $('input', entry).checked = style.enabled;
-
- const styleName = $('.style-name', entry);
- styleName.lastChild.textContent = style.customName || style.name;
- setTimeout(() => {
- styleName.title = entry.styleMeta.sloppy ?
- t('styleNotAppliedRegexpProblemTooltip') :
- styleName.scrollWidth > styleName.clientWidth + 1 ?
- styleName.textContent : '';
- });
-
- entry.classList.toggle('not-applied', style.excluded || style.sloppy);
- entry.classList.toggle('regexp-partial', style.sloppy);
-
- $('.exclude-by-domain-checkbox', entry).checked = styleExcluded(style, 'domain');
- $('.exclude-by-url-checkbox', entry).checked = styleExcluded(style, 'url');
-
- $('.exclude-by-domain', entry).title = getExcludeRule('domain');
- $('.exclude-by-url', entry).title = getExcludeRule('url');
-
- const {frameUrl} = style;
- if (frameUrl) {
- const sel = 'span.frame-url';
- const frameEl = $(sel, entry) || styleName.insertBefore($create(sel), styleName.lastChild);
- frameEl.title = frameUrl;
- frameEl.onmousedown = handleEvent.maybeEdit;
- }
- entry.classList.toggle('frame', Boolean(frameUrl));
-
- return entry;
-}
-
-function styleExcluded({exclusions}, type) {
- if (!exclusions) {
- return false;
- }
- const rule = getExcludeRule(type);
- return exclusions.includes(rule);
-}
-
-function getExcludeRule(type) {
- const u = new URL(tabURL);
- if (type === 'domain') {
- return u.origin + '/*';
- }
- // current page
- return escapeGlob(u.origin + u.pathname);
-}
-
-function escapeGlob(text) {
- return text.replace(/\*/g, '\\*');
-}
-
-Object.assign(handleEvent, {
-
- getClickedStyleId(event) {
- return (handleEvent.getClickedStyleElement(event) || {}).styleId;
- },
-
- getClickedStyleElement(event) {
- return event.target.closest('.entry');
- },
-
- name(event) {
- $('input', this).dispatchEvent(new MouseEvent('click'));
- event.preventDefault();
- },
-
- async toggle(event) {
- // when fired on checkbox, prevent the parent label from seeing the event, see #501
- event.stopPropagation();
- await API.styles.toggle(handleEvent.getClickedStyleId(event), this.checked);
- resortEntries();
- },
-
- toggleExclude(event, type) {
- const entry = handleEvent.getClickedStyleElement(event);
- if (event.target.checked) {
- API.styles.addExclusion(entry.styleMeta.id, getExcludeRule(type));
- } else {
- API.styles.removeExclusion(entry.styleMeta.id, getExcludeRule(type));
- }
- },
-
- toggleMenu(event) {
- const entry = handleEvent.getClickedStyleElement(event);
- const menu = $('.menu', entry);
- if (menu.hasAttribute(MODAL_SHOWN)) {
- hideModal(menu, {animate: true});
- } else {
- $('.menu-title', entry).textContent = $('.style-name', entry).textContent;
- showModal(menu, '.menu-close');
- }
- },
-
- delete(event) {
- const entry = handleEvent.getClickedStyleElement(event);
- const box = $('#confirm');
- box.dataset.id = entry.styleId;
- $('b', box).textContent = $('.style-name', entry).textContent;
- showModal(box, '[data-cmd=cancel]');
- },
-
- configure(event) {
- const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
- if (styleIsUsercss) {
- API.styles.get(styleId).then(style => {
- hotkeys.setState(false);
- configDialog(style).then(() => {
- hotkeys.setState(true);
- });
- });
- } else {
- handleEvent.openURLandHide.call(this, event);
- }
- },
-
- indicator(event) {
- const entry = handleEvent.getClickedStyleElement(event);
- const info = t.template.regexpProblemExplanation.cloneNode(true);
- $.remove('#' + info.id);
- $$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide));
- $$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation));
- entry.appendChild(info);
- },
-
- closeExplanation() {
- $('#regexp-explanation').remove();
- },
-
- openEditor(event, options) {
- event.preventDefault();
- API.openEditor(options);
- window.close();
- },
-
- maybeEdit(event) {
- if (!(
- event.button === 0 && (event.ctrlKey || event.metaKey) ||
- event.button === 1 ||
- event.button === 2)) {
- return;
- }
- // open an editor on middleclick
- const el = event.target;
- if (el.matches('.entry, .style-edit-link') || el.closest('.style-name')) {
- this.onmouseup = () => $('.style-edit-link', this).click();
- this.oncontextmenu = event => event.preventDefault();
- event.preventDefault();
- return;
- }
- // prevent the popup being opened in a background tab
- // when an irrelevant link was accidentally clicked
- if (el.closest('a')) {
- event.preventDefault();
- return;
- }
- },
-
- openURLandHide(event) {
- event.preventDefault();
- getActiveTab()
- .then(activeTab => API.openURL({
- url: this.href || this.dataset.href,
- index: activeTab.index + 1,
- message: tryJSONparse(this.dataset.sendMessage),
- }))
- .then(window.close);
- },
-
- openManager(event) {
- event.preventDefault();
- const isSearch = tabURL && (event.shiftKey || event.button === 2);
- API.openManage(isSearch ? {search: tabURL, searchMode: 'url'} : {});
- window.close();
- },
-
- copyContent(event) {
- event.preventDefault();
- const target = document.activeElement;
- const message = $('.copy-message');
- navigator.clipboard.writeText(target.textContent);
- target.classList.add('copied');
- message.classList.add('show-message');
+ const styleName = $('.style-name', entry);
+ styleName.lastChild.textContent = style.customName || style.name;
setTimeout(() => {
- target.classList.remove('copied');
- message.classList.remove('show-message');
- }, 1000);
- },
-});
+ styleName.title = entry.styleMeta.sloppy ?
+ t('styleNotAppliedRegexpProblemTooltip') :
+ styleName.scrollWidth > styleName.clientWidth + 1 ?
+ styleName.textContent : '';
+ });
+ entry.classList.toggle('not-applied', style.excluded || style.sloppy);
+ entry.classList.toggle('regexp-partial', style.sloppy);
-async function handleUpdate({style, reason}) {
- if (reason !== 'toggle' || !$.entry(style)) {
- style = await getStyleDataMerged(tabURL, style.id);
- if (!style) return;
- }
- const el = createStyleElement(style);
- if (!el.parentNode) {
- installed.appendChild(el);
- blockPopup(false);
- }
- resortEntries();
-}
+ $('.exclude-by-domain-checkbox', entry).checked = Events.isStyleExcluded(style, 'domain');
+ $('.exclude-by-url-checkbox', entry).checked = Events.isStyleExcluded(style, 'url');
+ $('.exclude-by-domain', entry).title = Events.getExcludeRule('domain');
+ $('.exclude-by-url', entry).title = Events.getExcludeRule('url');
-function handleDelete(id) {
- const el = $.entry(id);
- if (el) {
- el.remove();
- if (!$('.entry')) installed.appendChild(t.template.noStyles);
- }
-}
-
-function blockPopup(isBlocked = true) {
- document.body.classList.toggle('blocked', isBlocked);
- if (isBlocked) {
- document.body.prepend(t.template.unavailableInfo);
- } else {
- t.template.unavailableInfo.remove();
- t.template.noStyles.remove();
- }
-}
-
-function showModal(box, cancelButtonSelector) {
- const oldBox = $(`[${MODAL_SHOWN}]`);
- if (oldBox) box.style.animationName = 'none';
- // '' would be fine but 'true' is backward-compatible with the existing userstyles
- box.setAttribute(MODAL_SHOWN, 'true');
- box._onkeydown = e => {
- const key = getEventKeyName(e);
- switch (key) {
- case 'Tab':
- case 'Shift-Tab':
- e.preventDefault();
- moveFocus(box, e.shiftKey ? -1 : 1);
- break;
- case 'Escape': {
- e.preventDefault();
- window.onkeydown = null;
- $(cancelButtonSelector, box).click();
- break;
- }
+ const {frameUrl} = style;
+ if (frameUrl) {
+ const sel = 'span.frame-url';
+ const frameEl = $(sel, entry) || styleName.insertBefore($create(sel), styleName.lastChild);
+ frameEl.title = frameUrl;
+ frameEl.onmousedown = Events.maybeEdit;
}
- };
- window.on('keydown', box._onkeydown);
- moveFocus(box, 0);
- hideModal(oldBox);
-}
+ entry.classList.toggle('frame', Boolean(frameUrl));
-async function hideModal(box, {animate} = {}) {
- window.off('keydown', box._onkeydown);
- box._onkeydown = null;
- if (animate) {
- box.style.animationName = '';
- await animateElement(box, 'lights-on');
+ return entry;
}
- box.removeAttribute(MODAL_SHOWN);
-}
+
+ async function handleUpdate({style, reason}) {
+ if (reason !== 'toggle' || !$.entry(style)) {
+ style = await getStyleDataMerged(tabURL, style.id);
+ if (!style) return;
+ }
+ const el = createStyleElement(style);
+ if (!el.parentNode) {
+ installed.appendChild(el);
+ blockPopup(false);
+ }
+ resortEntries();
+ }
+
+ function handleDelete(id) {
+ const el = $.entry(id);
+ if (el) {
+ el.remove();
+ if (!$('.entry')) installed.appendChild(t.template.noStyles);
+ }
+ }
+
+ function blockPopup(isBlocked = true) {
+ document.body.classList.toggle('blocked', isBlocked);
+ if (isBlocked) {
+ document.body.prepend(t.template.unavailableInfo);
+ } else {
+ t.template.unavailableInfo.remove();
+ t.template.noStyles.remove();
+ }
+ }
+
+ return {
+ resortEntries,
+ };
+});
diff --git a/popup/preinit.js b/popup/preinit.js
new file mode 100644
index 00000000..0e788db3
--- /dev/null
+++ b/popup/preinit.js
@@ -0,0 +1,85 @@
+'use strict';
+
+define(require => {
+ const {API} = require('/js/msg');
+ const {URLS} = require('/js/toolbox');
+ require(['./popup']); // async
+
+ const ABOUT_BLANK = 'about:blank';
+
+ /* Merges the extra props from API into style data.
+ * When `id` is specified returns a single object otherwise an array */
+ async function getStyleDataMerged(url, id) {
+ const styles = (await API.styles.getByUrl(url, id))
+ .map(r => Object.assign(r.style, r));
+ return id ? styles[0] : styles;
+ }
+
+ /** @param {browser.webNavigation._GetAllFramesReturnDetails[]} frames */
+ function sortTabFrames(frames) {
+ const unknown = new Map(frames.map(f => [f.frameId, f]));
+ const known = new Map([[0, unknown.get(0) || {frameId: 0, url: ''}]]);
+ unknown.delete(0);
+ let lastSize = 0;
+ while (unknown.size !== lastSize) {
+ for (const [frameId, f] of unknown) {
+ if (known.has(f.parentFrameId)) {
+ unknown.delete(frameId);
+ if (!f.errorOccurred) known.set(frameId, f);
+ if (f.url === ABOUT_BLANK) f.url = known.get(f.parentFrameId).url;
+ }
+ }
+ lastSize = unknown.size; // guard against an infinite loop due to a weird frame structure
+ }
+ const sortedFrames = [...known.values(), ...unknown.values()];
+ const urls = new Set([ABOUT_BLANK]);
+ for (const f of sortedFrames) {
+ if (!f.url) f.url = '';
+ f.isDupe = urls.has(f.url);
+ urls.add(f.url);
+ }
+ return sortedFrames;
+ }
+
+ function waitForTabUrlFF(tab) {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(...[
+ function onUpdated(tabId, info, updatedTab) {
+ if (info.url && tabId === tab.id) {
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ resolve(updatedTab);
+ }
+ },
+ ...'UpdateFilter' in browser.tabs ? [{tabId: tab.id}] : [],
+ // TODO: remove both spreads and tabId check when strict_min_version >= 61
+ ]);
+ });
+ }
+
+ return {
+ ABOUT_BLANK,
+ getStyleDataMerged,
+ initializing: (async () => {
+ let [tab] = await browser.tabs.query({currentWindow: true, active: true});
+ if (!chrome.app && tab.status === 'loading' && tab.url === ABOUT_BLANK) {
+ tab = await waitForTabUrlFF(tab);
+ }
+ const frames = sortTabFrames(await browser.webNavigation.getAllFrames({tabId: tab.id}));
+ let url = tab.pendingUrl || tab.url || ''; // new Chrome uses pendingUrl while connecting
+ if (url === 'chrome://newtab/' && !URLS.chromeProtectsNTP) {
+ url = frames[0].url || '';
+ }
+ if (!URLS.supported(url)) {
+ url = '';
+ frames.length = 1;
+ }
+ frames[0].url = url;
+ const uniqFrames = frames.filter(f => f.url && !f.isDupe);
+ const styles = await Promise.all(uniqFrames.map(async ({url}) => ({
+ url,
+ styles: await getStyleDataMerged(url),
+ })));
+ return {frames, styles, url};
+ })(),
+ };
+});
diff --git a/popup/search-results.css b/popup/search.css
old mode 100755
new mode 100644
similarity index 98%
rename from popup/search-results.css
rename to popup/search.css
index c20847ec..127738b4
--- a/popup/search-results.css
+++ b/popup/search.css
@@ -1,3 +1,7 @@
+/* IMPORTANT!
+ This file is loaded dynamically when the inline search is invoked.
+ So don't put main popup's stuff here. */
+
body.search-results-shown {
overflow-y: auto;
overflow-x: hidden;
@@ -255,11 +259,6 @@ body.search-results-shown {
text-shadow: 0 1px 4px rgba(0, 0, 0, .5);
}
-#find-styles-inline-group label {
- position: relative;
- padding-left: 16px;
-}
-
#search-params {
display: flex;
position: relative;
diff --git a/popup/search-results.js b/popup/search.js
old mode 100755
new mode 100644
similarity index 94%
rename from popup/search-results.js
rename to popup/search.js
index dad898cb..8faaf6fd
--- a/popup/search-results.js
+++ b/popup/search.js
@@ -1,22 +1,20 @@
-/* global
- $
- $$
- $create
- API
- debounce
- download
- FIREFOX
- handleEvent
- prefs
- t
- tabURL
- tryCatch
- URLS
-*/
'use strict';
-window.addEventListener('showStyles:done', () => {
- if (!tabURL) return;
+define(require => {
+ const {API} = require('/js/msg');
+ const {
+ FIREFOX,
+ URLS,
+ debounce,
+ download,
+ tryCatch,
+ } = require('/js/toolbox');
+ const t = require('/js/localization');
+ const {$, $$, $create, $remove} = require('/js/dom');
+ const prefs = require('/js/prefs');
+ const Events = require('./events');
+ require('./search.css');
+
const RESULT_ID_PREFIX = 'search-result-';
const INDEX_URL = URLS.usoArchiveRaw + 'search-index.json';
const STYLUS_CATEGORY = 'chrome-extension';
@@ -61,26 +59,23 @@ window.addEventListener('showStyles:done', () => {
const show = sel => $class(sel).remove('hidden');
const hide = sel => $class(sel).add('hidden');
- Object.assign($('#find-styles-link'), {
- href: URLS.usoArchive,
+ const exports = {
+ /** @this {HTMLAnchorElement} */
onclick(event) {
if (!prefs.get('popup.findStylesInline') || dom.container) {
// use a less specific category if the inline search wasn't used yet
if (!category) calcCategory({retry: 1});
this.search = new URLSearchParams({category, search: $('#search-query').value});
- handleEvent.openURLandHide.call(this, event);
+ Events.openURLandHide.call(this, event);
return;
}
- event.preventDefault();
this.textContent = this.title;
this.title = '';
init();
calcCategory();
ready = start();
},
- });
-
- return;
+ };
function init() {
setTimeout(() => document.body.classList.add('search-results-shown'));
@@ -127,7 +122,7 @@ window.addEventListener('showStyles:done', () => {
if (FIREFOX) {
let lastShift;
- addEventListener('resize', () => {
+ window.on('resize', () => {
const scrollbarWidth = window.innerWidth - document.scrollingElement.clientWidth;
const shift = document.body.getBoundingClientRect().left;
if (!scrollbarWidth || shift === lastShift) return;
@@ -137,7 +132,7 @@ window.addEventListener('showStyles:done', () => {
}, {passive: true});
}
- addEventListener('styleDeleted', ({detail: {style: {id}}}) => {
+ window.on('styleDeleted', ({detail: {style: {id}}}) => {
restoreScrollPosition();
const result = results.find(r => r.installedStyleId === id);
if (result) {
@@ -146,7 +141,7 @@ window.addEventListener('showStyles:done', () => {
}
});
- addEventListener('styleAdded', async ({detail: {style}}) => {
+ window.on('styleAdded', async ({detail: {style}}) => {
restoreScrollPosition();
const usoId = calcUsoId(style) ||
calcUsoId(await API.styles.get(style.id));
@@ -285,7 +280,7 @@ window.addEventListener('showStyles:done', () => {
entry.id = RESULT_ID_PREFIX + id;
// title
Object.assign($('.search-result-title', entry), {
- onclick: handleEvent.openURLandHide,
+ onclick: Events.openURLandHide,
href: URLS.usoArchive + `?category=${category}&style=${id}`,
});
$('.search-result-title span', entry).textContent =
@@ -304,7 +299,7 @@ window.addEventListener('showStyles:done', () => {
textContent: author,
title: author,
href: URLS.usoArchive + '?author=' + encodeURIComponent(author).replace(/%20/g, '+'),
- onclick: handleEvent.openURLandHide,
+ onclick: Events.openURLandHide,
});
// rating
$('[data-type="rating"]', entry).dataset.class =
@@ -424,7 +419,7 @@ window.addEventListener('showStyles:done', () => {
} catch (reason) {
error(`Error while downloading usoID:${id}\nReason: ${reason}`);
}
- $.remove('.lds-spinner', entry);
+ $remove('.lds-spinner', entry);
installButton.disabled = false;
entry.style.pointerEvents = '';
}
@@ -449,7 +444,7 @@ window.addEventListener('showStyles:done', () => {
* @returns {boolean} true if the category has actually changed
*/
function calcCategory({retry} = {}) {
- const u = tryCatch(() => new URL(tabURL));
+ const u = tryCatch(() => new URL(Events.thisTab.url));
const old = category;
if (!u) {
// Invalid URL
@@ -479,7 +474,7 @@ window.addEventListener('showStyles:done', () => {
index = (await download(INDEX_URL, {responseType: 'json'}))
.filter(res => res.f === 'uso');
clearTimeout(timer);
- $.remove(':scope > .lds-spinner', dom.list);
+ $remove(':scope > .lds-spinner', dom.list);
return index;
}
@@ -533,4 +528,6 @@ window.addEventListener('showStyles:done', () => {
if (!res._year) res._year = new Date(res.u * 1000).getFullYear();
return res;
}
-}, {once: true});
+
+ return exports;
+});
diff --git a/tools/build-vendor.js b/tools/build-vendor.js
index 449ed144..e7620794 100644
--- a/tools/build-vendor.js
+++ b/tools/build-vendor.js
@@ -90,11 +90,10 @@ async function generateThemeList() {
/* Do not edit. This file is auto-generated by build-vendor.js */
'use strict';
- /* exported CODEMIRROR_THEMES */
- const CODEMIRROR_THEMES = [
+ define([], [
${
themes.map(t => ` '${t.replace(/'/g, '\\$&')}',\n`).join('')
- }];
+ }]);
` + '\n';
}
@@ -161,10 +160,3 @@ function generateList(list) {
return `* ${src}`;
}).join('\n');
}
-
-// Rename CodeMirror$1 -> CodeMirror for development purposes
-// FIXME: is this a workaround for old version of codemirror?
-// function renameCodeMirrorVariable(filePath) {
- // const file = fs.readFileSync(filePath, 'utf8');
- // fs.writeFileSync(filePath, file.replace(/CodeMirror\$1/g, 'CodeMirror'));
-// }
diff --git a/vendor-overwrites/codemirror-addon/match-highlighter.js b/vendor-overwrites/codemirror-addon/match-highlighter.js
index cf2a53b0..1d0cc5bc 100644
--- a/vendor-overwrites/codemirror-addon/match-highlighter.js
+++ b/vendor-overwrites/codemirror-addon/match-highlighter.js
@@ -23,7 +23,10 @@
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror", "./matchesonscrollbar"], mod);
+ define([
+ "/vendor/codemirror/lib/codemirror",
+ "/vendor/codemirror/addon/search/matchesonscrollbar",
+ ], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
diff --git a/vendor-overwrites/colorpicker/colorconverter.js b/vendor-overwrites/colorpicker/colorconverter.js
deleted file mode 100644
index 2889ef44..00000000
--- a/vendor-overwrites/colorpicker/colorconverter.js
+++ /dev/null
@@ -1,371 +0,0 @@
-'use strict';
-
-const colorConverter = (() => {
-
- return {
- parse,
- format,
- formatAlpha,
- RGBtoHSV,
- HSVtoRGB,
- HSLtoHSV,
- HSVtoHSL,
- constrainHue,
- snapToInt,
- ALPHA_DIGITS: 3,
- // NAMED_COLORS is added below
- };
-
- function format(color = '', type = color.type, hexUppercase) {
- if (!color || !type) return typeof color === 'string' ? color : '';
- const a = formatAlpha(color.a);
- const hasA = Boolean(a);
- if (type === 'rgb' && color.type === 'hsl') {
- color = HSVtoRGB(HSLtoHSV(color));
- }
- const {r, g, b, h, s, l} = color;
- switch (type) {
- case 'hex': {
- const rgbStr = (0x1000000 + (r << 16) + (g << 8) + (b | 0)).toString(16).slice(1);
- const aStr = hasA ? (0x100 + Math.round(a * 255)).toString(16).slice(1) : '';
- const hexStr = `#${rgbStr + aStr}`.replace(/^#(.)\1(.)\2(.)\3(?:(.)\4)?$/, '#$1$2$3$4');
- return hexUppercase ? hexStr.toUpperCase() : hexStr.toLowerCase();
- }
- case 'rgb':
- return hasA ?
- `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${a})` :
- `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
- case 'hsl':
- return hasA ?
- `hsla(${h}, ${s}%, ${l}%, ${a})` :
- `hsl(${h}, ${s}%, ${l}%)`;
- }
- }
-
- // Copied from _hexcolor() in parserlib.js
- function validateHex(color) {
- return /^#[a-f\d]+$/i.test(color) && [4, 5, 7, 9].some(n => color.length === n);
- }
-
- function validateRGB(nums) {
- const isPercentage = nums[0].endsWith('%');
- const valid = isPercentage ? validatePercentage : validateNum;
- return nums.slice(0, 3).every(valid);
- }
-
- function validatePercentage(s) {
- if (!s.endsWith('%')) return false;
- const n = Number(s.slice(0, -1));
- return n >= 0 && n <= 100;
- }
-
- function validateNum(s) {
- const n = Number(s);
- return n >= 0 && n <= 255;
- }
-
- function validateHSL(nums) {
- return validateAngle(nums[0]) && nums.slice(1, 3).every(validatePercentage);
- }
-
- function validateAngle(s) {
- return /^-?(\d+|\d*\.\d+)(deg|grad|rad|turn)?$/i.test(s);
- }
-
- function validateAlpha(alpha) {
- if (alpha.endsWith('%')) {
- return validatePercentage(alpha);
- }
- const n = Number(alpha);
- return n >= 0 && n <= 1;
- }
-
- function parse(str) {
- if (typeof str !== 'string') return;
- str = str.trim();
- if (!str) return;
-
- if (str[0] !== '#' && !str.includes('(')) {
- // eslint-disable-next-line no-use-before-define
- str = colorConverter.NAMED_COLORS.get(str);
- if (!str) return;
- }
-
- if (str[0] === '#') {
- if (!validateHex(str)) {
- return null;
- }
- str = str.slice(1);
- const [r, g, b, a = 255] = str.length <= 4 ?
- str.match(/(.)/g).map(c => parseInt(c + c, 16)) :
- str.match(/(..)/g).map(c => parseInt(c, 16));
- return {type: 'hex', r, g, b, a: a === 255 ? undefined : a / 255};
- }
-
- const [, type, value] = str.match(/^(rgb|hsl)a?\((.*?)\)|$/i);
- if (!type) return;
-
- const comma = value.includes(',') && !value.includes('/');
- const num = value.trim().split(comma ? /\s*,\s*/ : /\s+(?!\/)|\s*\/\s*/);
- if (num.length < 3 || num.length > 4) return;
- if (num[3] && !validateAlpha(num[3])) return null;
-
- let a = !num[3] ? 1 : parseFloat(num[3]) / (num[3].endsWith('%') ? 100 : 1);
- if (isNaN(a)) a = 1;
-
- const first = num[0];
- if (/rgb/i.test(type)) {
- if (!validateRGB(num)) {
- return null;
- }
- const k = first.endsWith('%') ? 2.55 : 1;
- const [r, g, b] = num.map(s => Math.round(parseFloat(s) * k));
- return {type: 'rgb', r, g, b, a};
- } else {
- if (!validateHSL(num)) {
- return null;
- }
- let h = parseFloat(first);
- if (first.endsWith('grad')) h *= 360 / 400;
- else if (first.endsWith('rad')) h *= 180 / Math.PI;
- else if (first.endsWith('turn')) h *= 360;
- const s = parseFloat(num[1]);
- const l = parseFloat(num[2]);
- return {type: 'hsl', h, s, l, a};
- }
- }
-
- function formatAlpha(a) {
- return isNaN(a) ? '' :
- (a + .5 * Math.pow(10, -colorConverter.ALPHA_DIGITS))
- .toFixed(colorConverter.ALPHA_DIGITS + 1)
- .slice(0, -1)
- .replace(/^0(?=\.[1-9])|^1\.0+?$|\.?0+$/g, '');
- }
-
- function RGBtoHSV({r, g, b, a}) {
- r /= 255;
- g /= 255;
- b /= 255;
- const MaxC = Math.max(r, g, b);
- const MinC = Math.min(r, g, b);
- const DeltaC = MaxC - MinC;
-
- let h =
- DeltaC === 0 ? 0 :
- MaxC === r ? 60 * (((g - b) / DeltaC) % 6) :
- MaxC === g ? 60 * (((b - r) / DeltaC) + 2) :
- MaxC === b ? 60 * (((r - g) / DeltaC) + 4) :
- 0;
- h = constrainHue(h);
- return {
- h,
- s: MaxC === 0 ? 0 : DeltaC / MaxC,
- v: MaxC,
- a,
- };
- }
-
- function HSVtoRGB({h, s, v}) {
- h = constrainHue(h) % 360;
- const C = s * v;
- const X = C * (1 - Math.abs((h / 60) % 2 - 1));
- const m = v - C;
- const [r, g, b] =
- h >= 0 && h < 60 ? [C, X, 0] :
- h >= 60 && h < 120 ? [X, C, 0] :
- h >= 120 && h < 180 ? [0, C, X] :
- h >= 180 && h < 240 ? [0, X, C] :
- h >= 240 && h < 300 ? [X, 0, C] :
- h >= 300 && h < 360 ? [C, 0, X] : [];
- return {
- r: snapToInt(Math.round((r + m) * 255)),
- g: snapToInt(Math.round((g + m) * 255)),
- b: snapToInt(Math.round((b + m) * 255)),
- };
- }
-
- function HSLtoHSV({h, s, l, a}) {
- const t = s * (l < 50 ? l : 100 - l) / 100;
- return {
- h: constrainHue(h),
- s: t + l ? 200 * t / (t + l) / 100 : 0,
- v: (t + l) / 100,
- a,
- };
- }
-
- function HSVtoHSL({h, s, v}) {
- const l = (2 - s) * v / 2;
- const t = l < .5 ? l * 2 : 2 - l * 2;
- return {
- h: Math.round(constrainHue(h)),
- s: Math.round(t ? s * v / t * 100 : 0),
- l: Math.round(l * 100),
- };
- }
-
- function constrainHue(h) {
- return h < 0 ? h % 360 + 360 :
- h > 360 ? h % 360 :
- h;
- }
-
- function snapToInt(num) {
- const int = Math.round(num);
- return Math.abs(int - num) < 1e-3 ? int : num;
- }
-})();
-
-colorConverter.NAMED_COLORS = new Map([
- ['transparent', 'rgba(0, 0, 0, 0)'],
- // CSS4 named colors
- ['aliceblue', '#f0f8ff'],
- ['antiquewhite', '#faebd7'],
- ['aqua', '#00ffff'],
- ['aquamarine', '#7fffd4'],
- ['azure', '#f0ffff'],
- ['beige', '#f5f5dc'],
- ['bisque', '#ffe4c4'],
- ['black', '#000000'],
- ['blanchedalmond', '#ffebcd'],
- ['blue', '#0000ff'],
- ['blueviolet', '#8a2be2'],
- ['brown', '#a52a2a'],
- ['burlywood', '#deb887'],
- ['cadetblue', '#5f9ea0'],
- ['chartreuse', '#7fff00'],
- ['chocolate', '#d2691e'],
- ['coral', '#ff7f50'],
- ['cornflowerblue', '#6495ed'],
- ['cornsilk', '#fff8dc'],
- ['crimson', '#dc143c'],
- ['cyan', '#00ffff'],
- ['darkblue', '#00008b'],
- ['darkcyan', '#008b8b'],
- ['darkgoldenrod', '#b8860b'],
- ['darkgray', '#a9a9a9'],
- ['darkgrey', '#a9a9a9'],
- ['darkgreen', '#006400'],
- ['darkkhaki', '#bdb76b'],
- ['darkmagenta', '#8b008b'],
- ['darkolivegreen', '#556b2f'],
- ['darkorange', '#ff8c00'],
- ['darkorchid', '#9932cc'],
- ['darkred', '#8b0000'],
- ['darksalmon', '#e9967a'],
- ['darkseagreen', '#8fbc8f'],
- ['darkslateblue', '#483d8b'],
- ['darkslategray', '#2f4f4f'],
- ['darkslategrey', '#2f4f4f'],
- ['darkturquoise', '#00ced1'],
- ['darkviolet', '#9400d3'],
- ['deeppink', '#ff1493'],
- ['deepskyblue', '#00bfff'],
- ['dimgray', '#696969'],
- ['dimgrey', '#696969'],
- ['dodgerblue', '#1e90ff'],
- ['firebrick', '#b22222'],
- ['floralwhite', '#fffaf0'],
- ['forestgreen', '#228b22'],
- ['fuchsia', '#ff00ff'],
- ['gainsboro', '#dcdcdc'],
- ['ghostwhite', '#f8f8ff'],
- ['gold', '#ffd700'],
- ['goldenrod', '#daa520'],
- ['gray', '#808080'],
- ['grey', '#808080'],
- ['green', '#008000'],
- ['greenyellow', '#adff2f'],
- ['honeydew', '#f0fff0'],
- ['hotpink', '#ff69b4'],
- ['indianred', '#cd5c5c'],
- ['indigo', '#4b0082'],
- ['ivory', '#fffff0'],
- ['khaki', '#f0e68c'],
- ['lavender', '#e6e6fa'],
- ['lavenderblush', '#fff0f5'],
- ['lawngreen', '#7cfc00'],
- ['lemonchiffon', '#fffacd'],
- ['lightblue', '#add8e6'],
- ['lightcoral', '#f08080'],
- ['lightcyan', '#e0ffff'],
- ['lightgoldenrodyellow', '#fafad2'],
- ['lightgray', '#d3d3d3'],
- ['lightgrey', '#d3d3d3'],
- ['lightgreen', '#90ee90'],
- ['lightpink', '#ffb6c1'],
- ['lightsalmon', '#ffa07a'],
- ['lightseagreen', '#20b2aa'],
- ['lightskyblue', '#87cefa'],
- ['lightslategray', '#778899'],
- ['lightslategrey', '#778899'],
- ['lightsteelblue', '#b0c4de'],
- ['lightyellow', '#ffffe0'],
- ['lime', '#00ff00'],
- ['limegreen', '#32cd32'],
- ['linen', '#faf0e6'],
- ['magenta', '#ff00ff'],
- ['maroon', '#800000'],
- ['mediumaquamarine', '#66cdaa'],
- ['mediumblue', '#0000cd'],
- ['mediumorchid', '#ba55d3'],
- ['mediumpurple', '#9370db'],
- ['mediumseagreen', '#3cb371'],
- ['mediumslateblue', '#7b68ee'],
- ['mediumspringgreen', '#00fa9a'],
- ['mediumturquoise', '#48d1cc'],
- ['mediumvioletred', '#c71585'],
- ['midnightblue', '#191970'],
- ['mintcream', '#f5fffa'],
- ['mistyrose', '#ffe4e1'],
- ['moccasin', '#ffe4b5'],
- ['navajowhite', '#ffdead'],
- ['navy', '#000080'],
- ['oldlace', '#fdf5e6'],
- ['olive', '#808000'],
- ['olivedrab', '#6b8e23'],
- ['orange', '#ffa500'],
- ['orangered', '#ff4500'],
- ['orchid', '#da70d6'],
- ['palegoldenrod', '#eee8aa'],
- ['palegreen', '#98fb98'],
- ['paleturquoise', '#afeeee'],
- ['palevioletred', '#db7093'],
- ['papayawhip', '#ffefd5'],
- ['peachpuff', '#ffdab9'],
- ['peru', '#cd853f'],
- ['pink', '#ffc0cb'],
- ['plum', '#dda0dd'],
- ['powderblue', '#b0e0e6'],
- ['purple', '#800080'],
- ['rebeccapurple', '#663399'],
- ['red', '#ff0000'],
- ['rosybrown', '#bc8f8f'],
- ['royalblue', '#4169e1'],
- ['saddlebrown', '#8b4513'],
- ['salmon', '#fa8072'],
- ['sandybrown', '#f4a460'],
- ['seagreen', '#2e8b57'],
- ['seashell', '#fff5ee'],
- ['sienna', '#a0522d'],
- ['silver', '#c0c0c0'],
- ['skyblue', '#87ceeb'],
- ['slateblue', '#6a5acd'],
- ['slategray', '#708090'],
- ['slategrey', '#708090'],
- ['snow', '#fffafa'],
- ['springgreen', '#00ff7f'],
- ['steelblue', '#4682b4'],
- ['tan', '#d2b48c'],
- ['teal', '#008080'],
- ['thistle', '#d8bfd8'],
- ['tomato', '#ff6347'],
- ['turquoise', '#40e0d0'],
- ['violet', '#ee82ee'],
- ['wheat', '#f5deb3'],
- ['white', '#ffffff'],
- ['whitesmoke', '#f5f5f5'],
- ['yellow', '#ffff00'],
- ['yellowgreen', '#9acd32'],
-]);
diff --git a/vendor-overwrites/csslint/csslint.js b/vendor-overwrites/csslint/csslint.js
deleted file mode 100644
index 9555890e..00000000
--- a/vendor-overwrites/csslint/csslint.js
+++ /dev/null
@@ -1,1778 +0,0 @@
-/*
-Modded by tophf
-========== Original disclaimer:
-
-Copyright (c) 2016 Nicole Sullivan and Nicholas C. Zakas. All rights reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the 'Software'), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-*/
-
-/* global parserlib */
-'use strict';
-
-//region Reporter
-
-class Reporter {
- /**
- * An instance of Report is used to report results of the
- * verification back to the main API.
- * @class Reporter
- * @constructor
- * @param {String[]} lines - The text lines of the source.
- * @param {Object} ruleset - The set of rules to work with, including if
- * they are errors or warnings.
- * @param {Object} allow - explicitly allowed lines
- * @param {[][]} ingore - list of line ranges to be ignored
- */
- constructor(lines, ruleset, allow, ignore) {
- this.messages = [];
- this.stats = [];
- this.lines = lines;
- this.ruleset = ruleset;
- this.allow = allow || {};
- this.ignore = ignore || [];
- }
-
- error(message, line, col, rule = {}) {
- this.messages.push({
- type: 'error',
- evidence: this.lines[line - 1],
- line, col,
- message,
- rule,
- });
- }
-
- report(message, line, col, rule) {
- if (line in this.allow && rule.id in this.allow[line] ||
- this.ignore.some(range => range[0] <= line && line <= range[1])) {
- return;
- }
- this.messages.push({
- type: this.ruleset[rule.id] === 2 ? 'error' : 'warning',
- evidence: this.lines[line - 1],
- line, col,
- message,
- rule,
- });
- }
-
- info(message, line, col, rule) {
- this.messages.push({
- type: 'info',
- evidence: this.lines[line - 1],
- line, col,
- message,
- rule,
- });
- }
-
- rollupError(message, rule) {
- this.messages.push({
- type: 'error',
- rollup: true,
- message,
- rule,
- });
- }
-
- rollupWarn(message, rule) {
- this.messages.push({
- type: 'warning',
- rollup: true,
- message,
- rule,
- });
- }
-
- stat(name, value) {
- this.stats[name] = value;
- }
-}
-
-//endregion
-//region CSSLint
-
-//eslint-disable-next-line no-var
-var CSSLint = (() => {
-
- const RX_EMBEDDED = /\/\*\s*csslint\s+((?:[^*]|\*(?!\/))+?)\*\//ig;
- const EBMEDDED_RULE_VALUE_MAP = {
- // error
- 'true': 2,
- '2': 2,
- // warning
- '': 1,
- '1': 1,
- // ignore
- 'false': 0,
- '0': 0,
- };
- const rules = [];
-
- // previous CSSLint overrides are used to decide whether the parserlib's cache should be reset
- let prevOverrides;
-
- return Object.assign(new parserlib.util.EventTarget(), {
-
- addRule(rule) {
- rules.push(rule);
- rules[rule.id] = rule;
- },
-
- clearRules() {
- rules.length = 0;
- },
-
- getRules() {
- return rules
- .slice()
- .sort((a, b) =>
- a.id < b.id ? -1 :
- a.id > b.id ? 1 : 0);
- },
-
- getRuleset() {
- const ruleset = {};
- // by default, everything is a warning
- for (const rule of rules) {
- ruleset[rule.id] = 1;
- }
- return ruleset;
- },
-
- /**
- * Starts the verification process for the given CSS text.
- * @param {String} text The CSS text to verify.
- * @param {Object} ruleset (Optional) List of rules to apply. If null, then
- * all rules are used. If a rule has a value of 1 then it's a warning,
- * a value of 2 means it's an error.
- * @return {Object} Results of the verification.
- */
- verify(text, ruleset) {
-
- if (!ruleset) ruleset = this.getRuleset();
-
- const allow = {};
- const ignore = [];
- RX_EMBEDDED.lastIndex =
- text.lastIndexOf('/*',
- text.indexOf('csslint',
- text.indexOf('/*') + 1 || text.length) + 1);
- if (RX_EMBEDDED.lastIndex >= 0) {
- ruleset = Object.assign({}, ruleset);
- applyEmbeddedOverrides(text, ruleset, allow, ignore);
- }
-
- const parser = new parserlib.css.Parser({
- starHack: true,
- ieFilters: true,
- underscoreHack: true,
- strict: false,
- });
-
- const reporter = new Reporter([], ruleset, allow, ignore);
-
- // always report parsing errors as errors
- ruleset.errors = 2;
- Object.keys(ruleset).forEach(id =>
- ruleset[id] &&
- rules[id] &&
- rules[id].init(parser, reporter));
-
- // TODO: when ruleset is unchanged we can try to invalidate only line ranges in 'allow' and 'ignore'
- const newOvr = [ruleset, allow, ignore];
- const reuseCache = !prevOverrides || JSON.stringify(prevOverrides) === JSON.stringify(newOvr);
- prevOverrides = newOvr;
-
- try {
- parser.parse(text, {reuseCache});
- } catch (ex) {
- reporter.error('Fatal error, cannot continue: ' + ex.message, ex.line, ex.col, {});
- }
-
- const report = {
- messages: reporter.messages,
- stats: reporter.stats,
- ruleset: reporter.ruleset,
- allow: reporter.allow,
- ignore: reporter.ignore,
- };
-
- // sort by line numbers, rollups at the bottom
- report.messages.sort((a, b) =>
- a.rollup && !b.rollup ? 1 :
- !a.rollup && b.rollup ? -1 :
- a.line - b.line);
-
- parserlib.cache.feedback(report);
-
- return report;
- },
- });
-
- // Example 1:
-
- /* csslint ignore:start */
- /*
- the chunk of code where errors won't be reported
- the chunk's start is hardwired to the line of the opening comment
- the chunk's end is hardwired to the line of the closing comment
- */
- /* csslint ignore:end */
-
- // Example 2:
- // allow rule violations on the current line:
-
- // foo: bar; /* csslint allow:rulename1,rulename2,... */
- /* csslint allow:rulename1,rulename2,... */ // foo: bar;
-
- // Example 3:
-
- /* csslint rulename1 */
- /* csslint rulename2:N */
- /* csslint rulename3:N, rulename4:N */
-
- /* entire code is affected;
- * comments futher down the code extend/override previous comments of this kind
- * values for N (without the backquotes):
- `2` or `true` means "error"
- `1` or omitted means "warning" (when omitting, the colon can be omitted too)
- `0` or `false` means "ignore"
- */
-
- function applyEmbeddedOverrides(text, ruleset, allow, ignore) {
- let ignoreStart = null;
- let ignoreEnd = null;
- let lineno = 0;
- let eol = -1;
- let m;
-
- while ((m = RX_EMBEDDED.exec(text))) {
- // account for the lines between the previous and current match
- while (eol <= m.index) {
- eol = text.indexOf('\n', eol + 1);
- if (eol < 0) eol = text.length;
- lineno++;
- }
-
- const ovr = m[1].toLowerCase();
- const cmd = ovr.split(':', 1)[0];
- const i = cmd.length + 1;
-
- switch (cmd.trim()) {
-
- case 'allow': {
- const allowRuleset = {};
- let num = 0;
- ovr.slice(i).split(',').forEach(allowRule => {
- allowRuleset[allowRule.trim()] = true;
- num++;
- });
- if (num) allow[lineno] = allowRuleset;
- break;
- }
-
- case 'ignore':
- if (ovr.includes('start')) {
- ignoreStart = ignoreStart || lineno;
- break;
- }
- if (ovr.includes('end')) {
- ignoreEnd = lineno;
- if (ignoreStart && ignoreEnd) {
- ignore.push([ignoreStart, ignoreEnd]);
- ignoreStart = ignoreEnd = null;
- }
- }
- break;
-
- default:
- ovr.slice(i).split(',').forEach(rule => {
- const pair = rule.split(':');
- const property = pair[0] || '';
- const value = pair[1] || '';
- const mapped = EBMEDDED_RULE_VALUE_MAP[value.trim()];
- ruleset[property.trim()] = mapped === undefined ? 1 : mapped;
- });
- }
- }
-
- // Close remaining ignore block, if any
- if (ignoreStart) {
- ignore.push([ignoreStart, lineno]);
- }
- }
-})();
-
-//endregion
-//region Util
-
-// expose for testing purposes
-CSSLint._Reporter = Reporter;
-
-CSSLint.Util = {
-
- indexOf(values, value) {
- if (typeof values.indexOf === 'function') {
- return values.indexOf(value);
- }
- for (let i = 0, len = values.length; i < len; i++) {
- if (values[i] === value) {
- return i;
- }
- }
- return -1;
- },
-
- registerBlockEvents(parser, start, end, property) {
- for (const e of [
- 'document',
- 'fontface',
- 'keyframerule',
- 'media',
- 'page',
- 'pagemargin',
- 'rule',
- 'supports',
- 'viewport',
- ]) {
- if (start) parser.addListener('start' + e, start);
- if (end) parser.addListener('end' + e, end);
- }
- if (property) parser.addListener('property', property);
- },
-};
-
-//endregion
-//region Rules
-
-CSSLint.addRule({
- id: 'adjoining-classes',
- name: 'Disallow adjoining classes',
- desc: "Don't use adjoining classes.",
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-adjoining-classes',
- browsers: 'IE6',
-
- init(parser, reporter) {
- parser.addListener('startrule', event => {
- for (const selector of event.selectors) {
- for (const part of selector.parts) {
- if (part.type !== parser.SELECTOR_PART_TYPE) continue;
- let classCount = 0;
- for (const modifier of part.modifiers) {
- classCount += modifier.type === 'class';
- if (classCount > 1) {
- reporter.report('Adjoining classes: ' + selector.text, part.line, part.col, this);
- }
- }
- }
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'box-model',
- name: 'Beware of broken box size',
- desc: "Don't use width or height when using padding or border.",
- url: 'https://github.com/CSSLint/csslint/wiki/Beware-of-box-model-size',
- browsers: 'All',
-
- init(parser, reporter) {
- const sizeProps = {
- width: [
- 'border',
- 'border-left',
- 'border-right',
- 'padding',
- 'padding-left',
- 'padding-right',
- ],
- height: [
- 'border',
- 'border-bottom',
- 'border-top',
- 'padding',
- 'padding-bottom',
- 'padding-top',
- ],
- };
- let properties = {};
- let boxSizing = false;
- let started = 0;
-
- const startRule = () => {
- started = 1;
- properties = {};
- boxSizing = false;
- };
-
- const property = event => {
- if (!started) return;
- const name = event.property.text.toLowerCase();
-
- if (sizeProps.width.includes(name) || sizeProps.height.includes(name)) {
-
- if (!/^0+\D*$/.test(event.value) &&
- (name !== 'border' || !/^none$/i.test(event.value))) {
- properties[name] = {
- line: event.property.line,
- col: event.property.col,
- value: event.value,
- };
- }
-
- } else if (/^(width|height)/i.test(name) &&
- /^(length|percentage)/.test(event.value.parts[0].type)) {
- properties[name] = 1;
-
- } else if (name === 'box-sizing') {
- boxSizing = true;
- }
- };
-
- const endRule = () => {
- started = 0;
- if (boxSizing) return;
-
- for (const size in sizeProps) {
- if (!properties[size]) continue;
-
- for (const prop of sizeProps[size]) {
- if (prop !== 'padding' || !properties[prop]) continue;
-
- const {value: {parts}, line, col} = properties[prop].value;
- if (parts.length !== 2 || Number(parts[0].value) !== 0) {
- reporter.report(`Using ${size} with ${prop} can sometimes make elements larger than you expect.`,
- line, col, this);
- }
- }
- }
- };
-
- CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property);
- },
-});
-
-CSSLint.addRule({
- id: 'box-sizing',
- name: 'Disallow use of box-sizing',
- desc: "The box-sizing properties isn't supported in IE6 and IE7.",
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-box-sizing',
- browsers: 'IE6, IE7',
- tags: ['Compatibility'],
-
- init(parser, reporter) {
- parser.addListener('property', event => {
- if (event.property.text.toLowerCase() === 'box-sizing') {
- reporter.report(this.desc, event.line, event.col, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'bulletproof-font-face',
- name: 'Use the bulletproof @font-face syntax',
- desc: 'Use the bulletproof @font-face syntax to avoid 404\'s in old IE ' +
- '(http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax).',
- url: 'https://github.com/CSSLint/csslint/wiki/Bulletproof-font-face',
- browsers: 'All',
-
- init(parser, reporter) {
- const regex = /^\s?url\(['"].+\.eot\?.*['"]\)\s*format\(['"]embedded-opentype['"]\).*$/i;
- let firstSrc = true;
- let ruleFailed = false;
- let line, col;
-
- // Mark the start of a @font-face declaration so we only test properties inside it
- parser.addListener('startfontface', () => {
- parser.addListener('property', property);
- });
-
- function property(event) {
- const propertyName = event.property.toString().toLowerCase();
- if (propertyName !== 'src') return;
-
- const value = event.value.toString();
- line = event.line;
- col = event.col;
-
- const matched = regex.test(value);
- if (firstSrc && !matched) {
- ruleFailed = true;
- firstSrc = false;
- } else if (!firstSrc && matched) {
- ruleFailed = false;
- }
- }
-
- // Back to normal rules that we don't need to test
- parser.addListener('endfontface', () => {
- parser.removeListener('property', property);
- if (!ruleFailed) return;
- reporter.report("@font-face declaration doesn't follow the fontspring bulletproof syntax.",
- line, col, this);
- });
- },
-});
-
-CSSLint.addRule({
- id: 'compatible-vendor-prefixes',
- name: 'Require compatible vendor prefixes',
- desc: 'Include all compatible vendor prefixes to reach a wider range of users.',
- url: 'https://github.com/CSSLint/csslint/wiki/Require-compatible-vendor-prefixes',
- browsers: 'All',
-
- init(parser, reporter) {
- // See http://peter.sh/experiments/vendor-prefixed-css-property-overview/ for details
- const compatiblePrefixes = {
- 'animation': 'webkit',
- 'animation-delay': 'webkit',
- 'animation-direction': 'webkit',
- 'animation-duration': 'webkit',
- 'animation-fill-mode': 'webkit',
- 'animation-iteration-count': 'webkit',
- 'animation-name': 'webkit',
- 'animation-play-state': 'webkit',
- 'animation-timing-function': 'webkit',
- 'appearance': 'webkit moz',
- 'border-end': 'webkit moz',
- 'border-end-color': 'webkit moz',
- 'border-end-style': 'webkit moz',
- 'border-end-width': 'webkit moz',
- 'border-image': 'webkit moz o',
- 'border-radius': 'webkit',
- 'border-start': 'webkit moz',
- 'border-start-color': 'webkit moz',
- 'border-start-style': 'webkit moz',
- 'border-start-width': 'webkit moz',
- 'box-align': 'webkit moz',
- 'box-direction': 'webkit moz',
- 'box-flex': 'webkit moz',
- 'box-lines': 'webkit',
- 'box-ordinal-group': 'webkit moz',
- 'box-orient': 'webkit moz',
- 'box-pack': 'webkit moz',
- 'box-sizing': '',
- 'box-shadow': '',
- 'column-count': 'webkit moz ms',
- 'column-gap': 'webkit moz ms',
- 'column-rule': 'webkit moz ms',
- 'column-rule-color': 'webkit moz ms',
- 'column-rule-style': 'webkit moz ms',
- 'column-rule-width': 'webkit moz ms',
- 'column-width': 'webkit moz ms',
- 'flex': 'webkit ms',
- 'flex-basis': 'webkit',
- 'flex-direction': 'webkit ms',
- 'flex-flow': 'webkit',
- 'flex-grow': 'webkit',
- 'flex-shrink': 'webkit',
- 'hyphens': 'epub moz',
- 'line-break': 'webkit ms',
- 'margin-end': 'webkit moz',
- 'margin-start': 'webkit moz',
- 'marquee-speed': 'webkit wap',
- 'marquee-style': 'webkit wap',
- 'padding-end': 'webkit moz',
- 'padding-start': 'webkit moz',
- 'tab-size': 'moz o',
- 'text-size-adjust': 'webkit ms',
- 'transform': 'webkit ms',
- 'transform-origin': 'webkit ms',
- 'transition': '',
- 'transition-delay': '',
- 'transition-duration': '',
- 'transition-property': '',
- 'transition-timing-function': '',
- 'user-modify': 'webkit moz',
- 'user-select': 'webkit moz ms',
- 'word-break': 'epub ms',
- 'writing-mode': 'epub ms',
- };
- const applyTo = [];
- let properties = [];
- let inKeyFrame = false;
- let started = 0;
-
- for (const prop in compatiblePrefixes) {
- const variations = compatiblePrefixes[prop].split(' ').map(s => `-${s}-${prop}`);
- compatiblePrefixes[prop] = variations;
- applyTo.push(...variations);
- }
-
- parser.addListener('startrule', () => {
- started++;
- properties = [];
- });
-
- parser.addListener('startkeyframes', event => {
- started++;
- inKeyFrame = event.prefix || true;
- if (inKeyFrame && typeof inKeyFrame === 'string') {
- inKeyFrame = '-' + inKeyFrame + '-';
- }
- });
-
- parser.addListener('endkeyframes', () => {
- started--;
- inKeyFrame = false;
- });
-
- parser.addListener('property', event => {
- if (!started) return;
- const name = event.property.text;
- if (inKeyFrame &&
- typeof inKeyFrame === 'string' &&
- name.startsWith(inKeyFrame) ||
- CSSLint.Util.indexOf(applyTo, name) < 0) {
- return;
- }
- properties.push(event.property);
- });
-
- parser.addListener('endrule', () => {
- started = 0;
- if (!properties.length) return;
- const propertyGroups = {};
-
- for (const name of properties) {
- for (const prop in compatiblePrefixes) {
- const variations = compatiblePrefixes[prop];
- if (CSSLint.Util.indexOf(variations, name.text) <= -1) continue;
-
- if (!propertyGroups[prop]) {
- propertyGroups[prop] = {
- full: variations.slice(0),
- actual: [],
- actualNodes: [],
- };
- }
-
- if (CSSLint.Util.indexOf(propertyGroups[prop].actual, name.text) === -1) {
- propertyGroups[prop].actual.push(name.text);
- propertyGroups[prop].actualNodes.push(name);
- }
- }
- }
-
- for (const prop in propertyGroups) {
- const value = propertyGroups[prop];
- const actual = value.actual;
- if (value.full.length <= actual.length) continue;
-
- for (const item of value.full) {
- if (CSSLint.Util.indexOf(actual, item) !== -1) continue;
-
- const propertiesSpecified =
- actual.length === 1 ?
- actual[0] :
- actual.length === 2 ?
- actual.join(' and ') :
- actual.join(', ');
-
- const {line, col} = value.actualNodes[0];
- reporter.report(
- `The property ${item} is compatible with ${propertiesSpecified} and should be included as well.`,
- line, col, this);
- }
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'display-property-grouping',
- name: 'Require properties appropriate for display',
- desc: "Certain properties shouldn't be used with certain display property values.",
- url: 'https://github.com/CSSLint/csslint/wiki/Require-properties-appropriate-for-display',
- browsers: 'All',
-
- init(parser, reporter) {
- const propertiesToCheck = {
- 'display': 1,
- 'float': 'none',
- 'height': 1,
- 'width': 1,
- 'margin': 1,
- 'margin-left': 1,
- 'margin-right': 1,
- 'margin-bottom': 1,
- 'margin-top': 1,
- 'padding': 1,
- 'padding-left': 1,
- 'padding-right': 1,
- 'padding-bottom': 1,
- 'padding-top': 1,
- 'vertical-align': 1,
- };
- let properties;
- let started = 0;
-
- const startRule = () => {
- started = 1;
- properties = {};
- };
-
- const property = event => {
- if (!started) return;
- const name = event.property.text.toLowerCase();
- if (name in propertiesToCheck) {
- properties[name] = {
- value: event.value.text,
- line: event.property.line,
- col: event.property.col,
- };
- }
- };
-
- const reportProperty = (name, display, msg) => {
- const prop = properties[name];
- if (!prop) return;
-
- const toCheck = propertiesToCheck[name];
- if (typeof toCheck === 'string' && toCheck === prop.value.toLowerCase()) return;
-
- const {line, col} = prop;
- reporter.report(msg || `${name} can't be used with display: ${display}.`,
- line, col, this);
- };
-
- const endRule = () => {
- started = 0;
- const display = properties.display && properties.display.value;
- if (!display) return;
-
- switch (display.toLowerCase()) {
-
- case 'inline':
- ['height', 'width', 'margin', 'margin-top', 'margin-bottom']
- .forEach(p => reportProperty(p, display));
-
- reportProperty('float', display,
- 'display:inline has no effect on floated elements ' +
- '(but may be used to fix the IE6 double-margin bug).');
- break;
-
- case 'block':
- // vertical-align should not be used with block
- reportProperty('vertical-align', display);
- break;
-
- case 'inline-block':
- // float should not be used with inline-block
- reportProperty('float', display);
- break;
-
- default:
- // margin, float should not be used with table
- if (display.indexOf('table-') !== 0) {
- return;
- }
- ['margin', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'float']
- .forEach(p => reportProperty(p, display));
- }
- };
-
- CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property);
- },
-});
-
-CSSLint.addRule({
- id: 'duplicate-background-images',
- name: 'Disallow duplicate background images',
- desc: 'Every background-image should be unique. Use a common class for e.g. sprites.',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-duplicate-background-images',
- browsers: 'All',
-
- init(parser, reporter) {
- const stack = {};
-
- parser.addListener('property', event => {
- const name = event.property.text;
- if (!name.match(/background/i)) return;
-
- for (const part of event.value.parts) {
- if (part.type !== 'uri') continue;
-
- const uri = stack[part.uri];
- if (uri === undefined) {
- stack[part.uri] = event;
- continue;
- }
-
- reporter.report(
- `Background image '${part.uri}' was used multiple times, ` +
- `first declared at line ${uri.line}, col ${uri.col}.`,
- event.line, event.col, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'duplicate-properties',
- name: 'Disallow duplicate properties',
- desc: 'Duplicate properties must appear one after the other.',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-duplicate-properties',
- browsers: 'All',
-
- init(parser, reporter) {
- let properties, lastName;
- let started = 0;
-
- const startRule = () => {
- started = 1;
- properties = {};
- };
-
- const endRule = () => {
- started = 0;
- properties = {};
- };
-
- const property = event => {
- if (!started) return;
- const property = event.property;
- const name = property.text.toLowerCase();
- const last = properties[name];
- if (last && (lastName !== name || last === event.value.text)) {
- reporter.report(`Duplicate property '${property}' found.`, event.line, event.col, this);
- }
- properties[name] = event.value.text;
- lastName = name;
- };
-
- CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property);
- },
-});
-
-CSSLint.addRule({
- id: 'empty-rules',
- name: 'Disallow empty rules',
- desc: 'Rules without any properties specified should be removed.',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-empty-rules',
- browsers: 'All',
-
- init(parser, reporter) {
- let count = 0;
- parser.addListener('startrule', () => (count = 0));
- parser.addListener('property', () => count++);
- parser.addListener('endrule', event => {
- if (!count) {
- const {line, col} = event.selectors[0];
- reporter.report('Rule is empty.', line, col, this);
- }
- });
- },
-
-});
-
-CSSLint.addRule({
- id: 'errors',
- name: 'Parsing Errors',
- desc: 'This rule looks for recoverable syntax errors.',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('error', ({message, line, col}) => {
- reporter.error(message, line, col, this);
- });
- },
-});
-
-CSSLint.addRule({
- id: 'warnings',
- name: 'Parsing warnings',
- desc: 'This rule looks for parser warnings.',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('warning', ({message, line, col}) => {
- reporter.report(message, line, col, this);
- });
- },
-});
-
-CSSLint.addRule({
- id: 'fallback-colors',
- name: 'Require fallback colors',
- desc: "For older browsers that don't support RGBA, HSL, or HSLA, provide a fallback color.",
- url: 'https://github.com/CSSLint/csslint/wiki/Require-fallback-colors',
- browsers: 'IE6,IE7,IE8',
-
- init(parser, reporter) {
- const propertiesToCheck = new Set([
- 'color',
- 'background',
- 'border-color',
- 'border-top-color',
- 'border-right-color',
- 'border-bottom-color',
- 'border-left-color',
- 'border',
- 'border-top',
- 'border-right',
- 'border-bottom',
- 'border-left',
- 'background-color',
- ]);
- let lastProperty;
- const startRule = () => (lastProperty = null);
-
- CSSLint.Util.registerBlockEvents(parser, startRule, null, event => {
- const name = event.property.text.toLowerCase();
- if (!propertiesToCheck.has(name)) {
- lastProperty = event;
- return;
- }
-
- let colorType = '';
- for (const part of event.value.parts) {
- if (part.type !== 'color') continue;
-
- if (!('alpha' in part || 'hue' in part)) {
- event.colorType = 'compat';
- continue;
- }
-
- if (/([^)]+)\(/.test(part)) {
- colorType = RegExp.$1.toUpperCase();
- }
-
- if (!lastProperty ||
- lastProperty.property.text.toLowerCase() !== name ||
- lastProperty.colorType !== 'compat') {
- reporter.report(`Fallback ${name} (hex or RGB) should precede ${colorType} ${name}.`,
- event.line, event.col, this);
- }
- }
- lastProperty = event;
- });
- },
-});
-
-CSSLint.addRule({
- id: 'floats',
- name: 'Disallow too many floats',
- desc: 'This rule tests if the float property is used too many times',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-too-many-floats',
- browsers: 'All',
-
- init(parser, reporter) {
- let count = 0;
-
- parser.addListener('property', ({property, value}) => {
- count +=
- property.text.toLowerCase() === 'float' &&
- value.text.toLowerCase() !== 'none';
- });
-
- parser.addListener('endstylesheet', () => {
- reporter.stat('floats', count);
- if (count >= 10) {
- reporter.rollupWarn(
- `Too many floats (${count}), you're probably using them for layout. ` +
- 'Consider using a grid system instead.', this);
- }
- });
- },
-
-});
-
-CSSLint.addRule({
- id: 'font-faces',
- name: "Don't use too many web fonts",
- desc: 'Too many different web fonts in the same stylesheet.',
- url: 'https://github.com/CSSLint/csslint/wiki/Don%27t-use-too-many-web-fonts',
- browsers: 'All',
-
- init(parser, reporter) {
- let count = 0;
- parser.addListener('startfontface', () => count++);
- parser.addListener('endstylesheet', () => {
- if (count > 5) {
- reporter.rollupWarn(`Too many @font-face declarations (${count}).`, this);
- }
- });
- },
-
-});
-
-CSSLint.addRule({
- id: 'font-sizes',
- name: 'Disallow too many font sizes',
- desc: 'Checks the number of font-size declarations.',
- url: 'https://github.com/CSSLint/csslint/wiki/Don%27t-use-too-many-font-size-declarations',
- browsers: 'All',
-
- init(parser, reporter) {
- let count = 0;
-
- parser.addListener('property', event => {
- count += event.property.toString() === 'font-size';
- });
-
- parser.addListener('endstylesheet', () => {
- reporter.stat('font-sizes', count);
- if (count >= 10) {
- reporter.rollupWarn('Too many font-size declarations (' + count + '), abstraction needed.', this);
- }
- });
- },
-
-});
-
-CSSLint.addRule({
-
- id: 'gradients',
- name: 'Require all gradient definitions',
- desc: 'When using a vendor-prefixed gradient, make sure to use them all.',
- url: 'https://github.com/CSSLint/csslint/wiki/Require-all-gradient-definitions',
- browsers: 'All',
-
- init(parser, reporter) {
- let gradients;
-
- parser.addListener('startrule', () => {
- gradients = {
- moz: 0,
- webkit: 0,
- oldWebkit: 0,
- o: 0,
- };
- });
-
- parser.addListener('property', event => {
- if (/-(moz|o|webkit)(?:-(?:linear|radial))-gradient/i.test(event.value)) {
- gradients[RegExp.$1] = 1;
- } else if (/-webkit-gradient/i.test(event.value)) {
- gradients.oldWebkit = 1;
- }
- });
-
- parser.addListener('endrule', event => {
- const missing = [];
- if (!gradients.moz) missing.push('Firefox 3.6+');
- if (!gradients.webkit) missing.push('Webkit (Safari 5+, Chrome)');
- if (!gradients.oldWebkit) missing.push('Old Webkit (Safari 4+, Chrome)');
- if (!gradients.o) missing.push('Opera 11.1+');
- if (missing.length && missing.length < 4) {
- const {line, col} = event.selectors[0];
- reporter.report(`Missing vendor-prefixed CSS gradients for ${missing.join(', ')}.`,
- line, col, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'ids',
- name: 'Disallow IDs in selectors',
- desc: 'Selectors should not contain IDs.',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-IDs-in-selectors',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('startrule', event => {
- for (const {line, col, parts} of event.selectors) {
- const idCount =
- parts.reduce((sum = 0, {type, modifiers}) =>
- type === parser.SELECTOR_PART_TYPE ?
- modifiers.reduce(sum, mod => sum + (mod.type === 'id')) :
- sum);
- if (idCount === 1) {
- reporter.report("Don't use IDs in selectors.", line, col, this);
- } else if (idCount > 1) {
- reporter.report(idCount + ' IDs in the selector, really?', line, col, this);
- }
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'import-ie-limit',
- name: '@import limit on IE6-IE9',
- desc: 'IE6-9 supports up to 31 @import per stylesheet',
- browsers: 'IE6, IE7, IE8, IE9',
-
- init(parser, reporter) {
- const MAX_IMPORT_COUNT = 31;
- let count = 0;
- parser.addListener('startpage', () => (count = 0));
- parser.addListener('import', () => count++);
- parser.addListener('endstylesheet', () => {
- if (count > MAX_IMPORT_COUNT) {
- reporter.rollupError(`Too many @import rules (${count}). IE6-9 supports up to 31 import per stylesheet.`, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'import',
- name: 'Disallow @import',
- desc: "Don't use @import, use instead.",
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-%40import',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('import', ({line, col}) => {
- reporter.report('@import prevents parallel downloads, use instead.', line, col, this);
- });
- },
-});
-
-CSSLint.addRule({
- id: 'important',
- name: 'Disallow !important',
- desc: 'Be careful when using !important declaration',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-%21important',
- browsers: 'All',
-
- init(parser, reporter) {
- let count = 0;
-
- parser.addListener('property', event => {
- if (!event.important) return;
- count++;
- reporter.report('Use of !important', event.line, event.col, this);
- });
-
- parser.addListener('endstylesheet', () => {
- reporter.stat('important', count);
- if (count >= 10) {
- reporter.rollupWarn(
- `Too many !important declarations (${count}), ` +
- 'try to use less than 10 to avoid specificity issues.', this);
- }
- });
- },
-
-});
-
-CSSLint.addRule({
- id: 'known-properties',
- name: 'Require use of known properties',
- desc: 'Properties should be known (listed in CSS3 specification) or be a vendor-prefixed property.',
- url: 'https://github.com/CSSLint/csslint/wiki/Require-use-of-known-properties',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('property', event => {
- if (event.invalid) {
- reporter.report(event.invalid.message, event.line, event.col, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'order-alphabetical',
- name: 'Alphabetical order',
- desc: 'Assure properties are in alphabetical order',
- browsers: 'All',
-
- init(parser, reporter) {
- let properties;
- let started = 0;
-
- const startRule = () => {
- started = 1;
- properties = [];
- };
-
- const property = event => {
- if (!started) return;
- const name = event.property.text;
- const lowerCasePrefixLessName = name.toLowerCase().replace(/^-.*?-/, '');
- properties.push(lowerCasePrefixLessName);
- };
-
- const endRule = event => {
- started = 0;
- if (properties.join(',') !== properties.sort().join(',')) {
- reporter.report("Rule doesn't have all its properties in alphabetical order.", event.line, event.col, this);
- }
- };
-
- CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property);
- },
-});
-
-CSSLint.addRule({
- id: 'outline-none',
- name: 'Disallow outline: none',
- desc: 'Use of outline: none or outline: 0 should be limited to :focus rules.',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-outline%3Anone',
- browsers: 'All',
- tags: ['Accessibility'],
-
- init(parser, reporter) {
- let lastRule;
-
- const startRule = event => {
- lastRule = !event.selectors ? null : {
- line: event.line,
- col: event.col,
- selectors: event.selectors,
- propCount: 0,
- outline: false,
- };
- };
-
- const property = event => {
- if (!lastRule) return;
- const name = event.property.text.toLowerCase();
- const value = event.value;
- lastRule.propCount++;
- if (name === 'outline' && /^(none|0)$/i.test(value)) {
- lastRule.outline = true;
- }
- };
-
- const endRule = () => {
- const {outline, selectors, propCount, line, col} = lastRule || {};
- lastRule = null;
- if (!outline) return;
- if (selectors.toString().toLowerCase().indexOf(':focus') === -1) {
- reporter.report('Outlines should only be modified using :focus.', line, col, this);
- } else if (propCount === 1) {
- reporter.report("Outlines shouldn't be hidden unless other visual changes are made.",
- line, col, this);
- }
- };
-
- CSSLint.Util.registerBlockEvents(parser, startRule, endRule, property);
- },
-});
-
-CSSLint.addRule({
- id: 'overqualified-elements',
- name: 'Disallow overqualified elements',
- desc: "Don't use classes or IDs with elements (a.foo or a#foo).",
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-overqualified-elements',
- browsers: 'All',
-
- init(parser, reporter) {
- const classes = {};
-
- parser.addListener('startrule', event => {
- for (const selector of event.selectors) {
- for (const part of selector.parts) {
- if (part.type !== parser.SELECTOR_PART_TYPE) continue;
- for (const mod of part.modifiers) {
- if (part.elementName && mod.type === 'id') {
- reporter.report('Element (' + part + ') is overqualified, just use ' + mod +
- ' without element name.', part.line, part.col, this);
- } else if (mod.type === 'class') {
- let classMods = classes[mod];
- if (!classMods) classMods = classes[mod] = [];
- classMods.push({modifier: mod, part});
- }
- }
- }
- }
- });
-
- // one use means that this is overqualified
- parser.addListener('endstylesheet', () => {
- for (const prop in classes) {
- const {part, modifier} = classes[prop][0];
- if (part.elementName && classes[prop].length === 1) {
- reporter.report(`Element (${part}) is overqualified, just use ${modifier} without element name.`,
- part.line, part.col, this);
- }
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'qualified-headings',
- name: 'Disallow qualified headings',
- desc: 'Headings should not be qualified (namespaced).',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-qualified-headings',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('startrule', event => {
- for (const selector of event.selectors) {
- let first = true;
- for (const part of selector.parts) {
- const name = part.elementName;
- if (!first &&
- name &&
- part.type === parser.SELECTOR_PART_TYPE &&
- /h[1-6]/.test(name.toString())) {
- reporter.report(`Heading (${name}) should not be qualified.`,
- part.line, part.col, this);
- }
- first = false;
- }
- }
- });
- },
-
-});
-
-CSSLint.addRule({
- id: 'regex-selectors',
- name: 'Disallow selectors that look like regexs',
- desc: 'Selectors that look like regular expressions are slow and should be avoided.',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-selectors-that-look-like-regular-expressions',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('startrule', event => {
- for (const selector of event.selectors) {
- for (const part of selector.parts) {
- if (part.type !== parser.SELECTOR_PART_TYPE) continue;
- for (const mod of part.modifiers) {
- if (mod.type !== 'attribute' || !/([~|^$*]=)/.test(mod)) continue;
- reporter.report(`Attribute selectors with ${RegExp.$1} are slow!`,
- mod.line, mod.col, this);
- }
- }
- }
- });
- },
-
-});
-
-CSSLint.addRule({
- id: 'rules-count',
- name: 'Rules Count',
- desc: 'Track how many rules there are.',
- browsers: 'All',
-
- init(parser, reporter) {
- let count = 0;
- parser.addListener('startrule', () => count++);
- parser.addListener('endstylesheet', () => reporter.stat('rule-count', count));
- },
-});
-
-CSSLint.addRule({
- id: 'selector-max-approaching',
- name: 'Warn when approaching the 4095 selector limit for IE',
- desc: 'Will warn when selector count is >= 3800 selectors.',
- browsers: 'IE',
-
- init(parser, reporter) {
- let count = 0;
- parser.addListener('startrule', event => (count += event.selectors.length));
- parser.addListener('endstylesheet', () => {
- if (count >= 3800) {
- reporter.report(
- `You have ${count} selectors. ` +
- 'Internet Explorer supports a maximum of 4095 selectors per stylesheet. ' +
- 'Consider refactoring.', 0, 0, this);
- }
- });
- },
-
-});
-
-CSSLint.addRule({
- id: 'selector-max',
- name: 'Error when past the 4095 selector limit for IE',
- desc: 'Will error when selector count is > 4095.',
- browsers: 'IE',
-
- init(parser, reporter) {
- let count = 0;
- parser.addListener('startrule', event => (count += event.selectors.length));
- parser.addListener('endstylesheet', () => {
- if (count > 4095) {
- reporter.report(
- `You have ${count} selectors. ` +
- 'Internet Explorer supports a maximum of 4095 selectors per stylesheet. ' +
- 'Consider refactoring.', 0, 0, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'selector-newline',
- name: 'Disallow new-line characters in selectors',
- desc: 'New-line characters in selectors are usually a forgotten comma and not a descendant combinator.',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('startrule', event => {
- for (const {parts} of event.selectors) {
- for (let p = 0, pLen = parts.length; p < pLen; p++) {
- for (let n = p + 1; n < pLen; n++) {
- if (parts[p].type === 'descendant' &&
- parts[n].line > parts[p].line) {
- reporter.report('newline character found in selector (forgot a comma?)',
- parts[p].line, parts[0].col, this);
- }
- }
- }
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'shorthand',
- name: 'Require shorthand properties',
- desc: 'Use shorthand properties where possible.',
- url: 'https://github.com/CSSLint/csslint/wiki/Require-shorthand-properties',
- browsers: 'All',
-
- init(parser, reporter) {
- const propertiesToCheck = {};
- const mapping = {
- margin: ['margin-top', 'margin-bottom', 'margin-left', 'margin-right'],
- padding: ['padding-top', 'padding-bottom', 'padding-left', 'padding-right'],
- };
- let properties;
- let started = 0;
-
- for (const short in mapping) {
- for (const full of mapping[short]) {
- propertiesToCheck[full] = short;
- }
- }
-
- const startRule = () => {
- started = 1;
- properties = {};
- };
-
- const property = event => {
- if (!started) return;
- const name = event.property.toString().toLowerCase();
- if (name in propertiesToCheck) {
- properties[name] = 1;
- }
- };
-
- const endRule = event => {
- started = 0;
- for (const short in mapping) {
- const fullList = mapping[short];
- const total = fullList.reduce((sum = 0, name) => sum + (properties[name] ? 1 : 0));
- if (total === fullList.length) {
- reporter.report(`The properties ${fullList.join(', ')} can be replaced by ${short}.`,
- event.line, event.col, this);
- }
- }
- };
-
- parser.addListener('startrule', startRule);
- parser.addListener('startfontface', startRule);
- parser.addListener('property', property);
- parser.addListener('endrule', endRule);
- parser.addListener('endfontface', endRule);
- },
-});
-
-CSSLint.addRule({
- id: 'star-property-hack',
- name: 'Disallow properties with a star prefix',
- desc: 'Checks for the star property hack (targets IE6/7)',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-star-hack',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('property', ({property: {hack, line, col}}) => {
- if (hack === '*') {
- reporter.report('Property with star prefix found.', line, col, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'text-indent',
- name: 'Disallow negative text-indent',
- desc: 'Checks for text indent less than -99px',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-negative-text-indent',
- browsers: 'All',
-
- init(parser, reporter) {
- let textIndent, direction;
-
- const startRule = () => {
- textIndent = false;
- direction = 'inherit';
- };
-
- const endRule = () => {
- if (textIndent && direction !== 'ltr') {
- reporter.report(
- "Negative text-indent doesn't work well with RTL. " +
- 'If you use text-indent for image replacement explicitly set direction for that item to ltr.',
- textIndent.line, textIndent.col, this);
- }
- };
-
- parser.addListener('startrule', startRule);
- parser.addListener('startfontface', startRule);
-
- parser.addListener('property', event => {
- const name = event.property.toString().toLowerCase();
- const value = event.value;
-
- if (name === 'text-indent' && value.parts[0].value < -99) {
- textIndent = event.property;
- } else if (name === 'direction' && value.toString().toLowerCase() === 'ltr') {
- direction = 'ltr';
- }
- });
-
- parser.addListener('endrule', endRule);
- parser.addListener('endfontface', endRule);
- },
-});
-
-CSSLint.addRule({
- id: 'underscore-property-hack',
- name: 'Disallow properties with an underscore prefix',
- desc: 'Checks for the underscore property hack (targets IE6)',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-underscore-hack',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('property', ({property: {hack, line, col}}) => {
- if (hack === '_') {
- reporter.report('Property with underscore prefix found.', line, col, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'unique-headings',
- name: 'Headings should only be defined once',
- desc: 'Headings should be defined only once.',
- url: 'https://github.com/CSSLint/csslint/wiki/Headings-should-only-be-defined-once',
- browsers: 'All',
-
- init(parser, reporter) {
- const headings = new Array(6).fill(0);
-
- parser.addListener('startrule', event => {
- for (const {parts} of event.selectors) {
- const part = parts[parts.length - 1];
- if (!part.elementName || !/h([1-6])/i.test(part.elementName)) continue;
- if (part.modifiers.some(mod => mod.type === 'pseudo')) continue;
- if (++headings[Number(RegExp.$1) - 1] > 1) {
- reporter.report(`Heading (${part.elementName}) has already been defined.`,
- part.line, part.col, this);
- }
- }
- });
-
- parser.addListener('endstylesheet', () => {
- const messages = headings
- .filter(h => h > 1)
- .map((h, i) => `${h} H${i + 1}s`);
- if (messages.length) {
- reporter.rollupWarn(`You have ${messages.join(', ')} defined in this stylesheet.`, this);
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'universal-selector',
- name: 'Disallow universal selector',
- desc: 'The universal selector (*) is known to be slow.',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-universal-selector',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('startrule', event => {
- for (const {parts} of event.selectors) {
- const part = parts[parts.length - 1];
- if (part.elementName === '*') {
- reporter.report(this.desc, part.line, part.col, this);
- }
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'unqualified-attributes',
- name: 'Disallow unqualified attribute selectors',
- desc: 'Unqualified attribute selectors are known to be slow.',
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-unqualified-attribute-selectors',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('startrule', event => {
- for (const {parts} of event.selectors) {
- const part = parts[parts.length - 1];
- if (part.type !== parser.SELECTOR_PART_TYPE) continue;
- if (part.modifiers.some(mod => mod.type === 'class' || mod.type === 'id')) continue;
-
- const isUnqualified = !part.elementName || part.elementName === '*';
- for (const mod of part.modifiers) {
- if (mod.type === 'attribute' && isUnqualified) {
- reporter.report(this.desc, part.line, part.col, this);
- }
- }
- }
- });
- },
-});
-
-CSSLint.addRule({
- id: 'vendor-prefix',
- name: 'Require standard property with vendor prefix',
- desc: 'When using a vendor-prefixed property, make sure to include the standard one.',
- url: 'https://github.com/CSSLint/csslint/wiki/Require-standard-property-with-vendor-prefix',
- browsers: 'All',
-
- init(parser, reporter) {
- const propertiesToCheck = {
- '-webkit-border-radius': 'border-radius',
- '-webkit-border-top-left-radius': 'border-top-left-radius',
- '-webkit-border-top-right-radius': 'border-top-right-radius',
- '-webkit-border-bottom-left-radius': 'border-bottom-left-radius',
- '-webkit-border-bottom-right-radius': 'border-bottom-right-radius',
-
- '-o-border-radius': 'border-radius',
- '-o-border-top-left-radius': 'border-top-left-radius',
- '-o-border-top-right-radius': 'border-top-right-radius',
- '-o-border-bottom-left-radius': 'border-bottom-left-radius',
- '-o-border-bottom-right-radius': 'border-bottom-right-radius',
-
- '-moz-border-radius': 'border-radius',
- '-moz-border-radius-topleft': 'border-top-left-radius',
- '-moz-border-radius-topright': 'border-top-right-radius',
- '-moz-border-radius-bottomleft': 'border-bottom-left-radius',
- '-moz-border-radius-bottomright': 'border-bottom-right-radius',
-
- '-moz-column-count': 'column-count',
- '-webkit-column-count': 'column-count',
-
- '-moz-column-gap': 'column-gap',
- '-webkit-column-gap': 'column-gap',
-
- '-moz-column-rule': 'column-rule',
- '-webkit-column-rule': 'column-rule',
-
- '-moz-column-rule-style': 'column-rule-style',
- '-webkit-column-rule-style': 'column-rule-style',
-
- '-moz-column-rule-color': 'column-rule-color',
- '-webkit-column-rule-color': 'column-rule-color',
-
- '-moz-column-rule-width': 'column-rule-width',
- '-webkit-column-rule-width': 'column-rule-width',
-
- '-moz-column-width': 'column-width',
- '-webkit-column-width': 'column-width',
-
- '-webkit-column-span': 'column-span',
- '-webkit-columns': 'columns',
-
- '-moz-box-shadow': 'box-shadow',
- '-webkit-box-shadow': 'box-shadow',
-
- '-moz-transform': 'transform',
- '-webkit-transform': 'transform',
- '-o-transform': 'transform',
- '-ms-transform': 'transform',
-
- '-moz-transform-origin': 'transform-origin',
- '-webkit-transform-origin': 'transform-origin',
- '-o-transform-origin': 'transform-origin',
- '-ms-transform-origin': 'transform-origin',
-
- '-moz-box-sizing': 'box-sizing',
- '-webkit-box-sizing': 'box-sizing',
- };
- let properties, num, started;
-
- const startRule = () => {
- started = 1;
- properties = {};
- num = 1;
- };
-
- const endRule = () => {
- started = 0;
- const needsStandard = [];
-
- for (const prop in properties) {
- if (prop in propertiesToCheck) {
- needsStandard.push({
- actual: prop,
- needed: propertiesToCheck[prop],
- });
- }
- }
-
- for (const {needed, actual} of needsStandard) {
- const {line, col} = properties[actual][0].name;
- if (!properties[needed]) {
- reporter.report(`Missing standard property '${needed}' to go along with '${actual}'.`,
- line, col, this);
- } else if (properties[needed][0].pos < properties[actual][0].pos) {
- reporter.report(`Standard property '${needed}' should come after vendor-prefixed property '${actual}'.`,
- line, col, this);
- }
- }
- };
-
- CSSLint.Util.registerBlockEvents(parser, startRule, endRule, event => {
- if (!started) return;
- const name = event.property.text.toLowerCase();
- let prop = properties[name];
- if (!prop) prop = properties[name] = [];
- prop.push({
- name: event.property,
- value: event.value,
- pos: num++,
- });
- });
- },
-});
-
-CSSLint.addRule({
- id: 'zero-units',
- name: 'Disallow units for 0 values',
- desc: "You don't need to specify units when a value is 0.",
- url: 'https://github.com/CSSLint/csslint/wiki/Disallow-units-for-zero-values',
- browsers: 'All',
-
- init(parser, reporter) {
- parser.addListener('property', event => {
- for (const {units, type, value, line, col} of event.value.parts) {
- if ((units || type === 'percentage') && value === 0 && type !== 'time') {
- reporter.report("Values of 0 shouldn't have units specified.", line, col, this);
- }
- }
- });
- },
-});
-
-//endregion