diff --git a/.eslintrc b/.eslintrc
index e81cbc5d..cee33bfa 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -15,17 +15,25 @@ globals:
FIREFOX: false
OPERA: false
URLS: false
+ BG: false
notifyAllTabs: false
- refreshAllTabs: false
- updateIcon: false
getActiveTab: false
getActiveTabRealURL: false
getTabRealURL: false
openURL: false
activateTab: false
stringAsRegExp: false
- wildcardAsRegExp: false
ignoreChromeError: false
+ tryCatch: false
+ tryRegExp: false
+ tryJSONparse: false
+ debounce: false
+ deepCopy: false
+ onBackgroundReady: false
+ deleteStyleSafe: false
+ getStylesSafe: false
+ saveStyleSafe: false
+ sessionStorageHash: false
# localization.js
template: false
t: false
@@ -37,31 +45,13 @@ globals:
# dom.js
onDOMready: false
scrollElementIntoView: false
+ enforceInputRange: false
animateElement: false
$: false
$$: false
- # storage.js
+ # prefs.js
prefs: false
- cachedStyles: false
- sessionStorageHash: false
- getStylesSafe: false
- invalidateCache: false
- saveStyle: false
- enableStyle: false
- deleteStyle: false
- fixBoolean: false
- getDomains: false
- getType: false
- getApplicableSections: false
- isCheckbox: false
- runTryCatch: false
- tryRegExp: false
- tryJSONparse: false
- debounce: false
setupLivePrefs: false
- enforceInputRange: false
- getCodeMirrorThemes: false
- styleSectionsEqual: false
rules:
accessor-pairs: [2]
diff --git a/background.js b/background.js
index 415f1b3b..9b24a668 100644
--- a/background.js
+++ b/background.js
@@ -1,4 +1,4 @@
-/* global getDatabase, getStyles, reportError */
+/* global getDatabase, getStyles, saveStyle, reportError, invalidateCache */
'use strict';
chrome.webNavigation.onBeforeNavigate.addListener(data => {
@@ -39,9 +39,9 @@ function webNavigationListener(method, data) {
// messaging
-chrome.runtime.onMessage.addListener(onBackgroundMessage);
+chrome.runtime.onMessage.addListener(onRuntimeMessage);
-function onBackgroundMessage(request, sender, sendResponse) {
+function onRuntimeMessage(request, sender, sendResponse) {
switch (request.method) {
case 'getStyles':
@@ -61,9 +61,7 @@ function onBackgroundMessage(request, sender, sendResponse) {
return KEEP_CHANNEL_OPEN;
case 'invalidateCache':
- if (typeof invalidateCache != 'undefined') {
- invalidateCache(false, request);
- }
+ invalidateCache(false, request);
break;
case 'healthCheck':
@@ -101,8 +99,8 @@ if ('commands' in chrome) {
}
// context menus
-
-const contextMenus = {
+// eslint-disable-next-line no-var
+var contextMenus = {
'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
@@ -123,7 +121,7 @@ const contextMenus = {
// Vivaldi: Vivaldi/#
if (/Vivaldi\/[\d.]+$/.test(navigator.userAgent)
|| /Safari\/[\d.]+$/.test(navigator.userAgent)
- && ![...navigator.plugins].some(p => p.name == 'Shockwave Flash')) {
+ && !Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash')) {
contextMenus.editDeleteText = {
title: 'editDeleteText',
contexts: ['editable'],
@@ -172,8 +170,11 @@ chrome.tabs.onAttached.addListener((tabId, data) => {
});
});
-var codeMirrorThemes; // eslint-disable-line no-var
-getCodeMirrorThemes(themes => (codeMirrorThemes = themes));
+// eslint-disable-next-line no-var
+var codeMirrorThemes;
+getCodeMirrorThemes().then(themes => {
+ codeMirrorThemes = themes;
+});
// do not use prefs.get('version', null) as it might not yet be available
chrome.storage.local.get('version', prefs => {
@@ -198,6 +199,9 @@ chrome.storage.local.get('version', prefs => {
injectContentScripts();
function injectContentScripts() {
+ // expand * as .*?
+ const wildcardAsRegExp = (s, flags) =>
+ new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags);
const contentScripts = chrome.runtime.getManifest().content_scripts;
for (const cs of contentScripts) {
cs.matches = cs.matches.map(m => (
@@ -227,3 +231,152 @@ function injectContentScripts() {
}
});
}
+
+
+function refreshAllTabs() {
+ return new Promise(resolve => {
+ // list all tabs including chrome-extension:// which can be ours
+ chrome.tabs.query({}, tabs => {
+ const lastTab = tabs[tabs.length - 1];
+ for (const tab of tabs) {
+ getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => {
+ const message = {method: 'styleReplaceAll', styles};
+ chrome.tabs.sendMessage(tab.id, message);
+ updateIcon(tab, styles);
+ if (tab == lastTab) {
+ resolve();
+ }
+ });
+ }
+ });
+ });
+}
+
+
+function updateIcon(tab, styles) {
+ // while NTP is still loading only process the request for its main frame with a real url
+ // (but when it's loaded we should process style toggle requests from popups, for example)
+ const isNTP = tab.url == 'chrome://newtab/';
+ if (isNTP && tab.status != 'complete' || tab.id < 0) {
+ return;
+ }
+ if (styles) {
+ // check for not-yet-existing tabs e.g. omnibox instant search
+ chrome.tabs.get(tab.id, () => {
+ if (!chrome.runtime.lastError) {
+ stylesReceived(styles);
+ }
+ });
+ return;
+ }
+ if (isNTP) {
+ getTabRealURL(tab).then(url =>
+ getStyles({matchUrl: url, enabled: true, asHash: true}, stylesReceived));
+ } else {
+ getStyles({matchUrl: tab.url, enabled: true, asHash: true}, stylesReceived);
+ }
+
+ function stylesReceived(styles) {
+ let numStyles = styles.length;
+ if (numStyles === undefined) {
+ // for 'styles' asHash:true fake the length by counting numeric ids manually
+ numStyles = 0;
+ for (const id of Object.keys(styles)) {
+ numStyles += id.match(/^\d+$/) ? 1 : 0;
+ }
+ }
+ const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
+ const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : '';
+ const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
+ const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
+ chrome.browserAction.setIcon({
+ tabId: tab.id,
+ path: {
+ // Material Design 2016 new size is 16px
+ 16: `images/icon/16${postfix}.png`,
+ 32: `images/icon/32${postfix}.png`,
+ // Chromium forks or non-chromium browsers may still use the traditional 19px
+ 19: `images/icon/19${postfix}.png`,
+ 38: `images/icon/38${postfix}.png`,
+ // TODO: add Edge preferred sizes: 20, 25, 30, 40
+ },
+ }, () => {
+ if (!chrome.runtime.lastError) {
+ // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
+ chrome.browserAction.setBadgeBackgroundColor({color});
+ chrome.browserAction.setBadgeText({text, tabId: tab.id});
+ }
+ });
+ }
+}
+
+
+function getCodeMirrorThemes() {
+ if (!chrome.runtime.getPackageDirectoryEntry) {
+ return Promise.resolve([
+ '3024-day',
+ '3024-night',
+ 'abcdef',
+ 'ambiance',
+ 'ambiance-mobile',
+ 'base16-dark',
+ 'base16-light',
+ 'bespin',
+ 'blackboard',
+ 'cobalt',
+ 'colorforth',
+ 'dracula',
+ 'duotone-dark',
+ 'duotone-light',
+ 'eclipse',
+ 'elegant',
+ 'erlang-dark',
+ 'hopscotch',
+ 'icecoder',
+ 'isotope',
+ 'lesser-dark',
+ 'liquibyte',
+ 'material',
+ 'mbo',
+ 'mdn-like',
+ 'midnight',
+ 'monokai',
+ 'neat',
+ 'neo',
+ 'night',
+ 'panda-syntax',
+ 'paraiso-dark',
+ 'paraiso-light',
+ 'pastel-on-dark',
+ 'railscasts',
+ 'rubyblue',
+ 'seti',
+ 'solarized',
+ 'the-matrix',
+ 'tomorrow-night-bright',
+ 'tomorrow-night-eighties',
+ 'ttcn',
+ 'twilight',
+ 'vibrant-ink',
+ 'xq-dark',
+ 'xq-light',
+ 'yeti',
+ 'zenburn',
+ ]);
+ }
+ return new Promise(resolve => {
+ chrome.runtime.getPackageDirectoryEntry(rootDir => {
+ rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => {
+ themeDir.createReader().readEntries(entries => {
+ resolve([
+ chrome.i18n.getMessage('defaultTheme')
+ ].concat(
+ entries.filter(entry => entry.isFile)
+ .sort((a, b) => (a.name < b.name ? -1 : 1))
+ .map(entry => entry.name.replace(/\.css$/, ''))
+ ));
+ });
+ });
+ });
+ });
+}
diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js
index 6bfdd049..a052516c 100644
--- a/backup/fileSaveLoad.js
+++ b/backup/fileSaveLoad.js
@@ -1,4 +1,4 @@
-/* global messageBox */
+/* global messageBox, handleUpdate */
'use strict';
const STYLISH_DUMP_FILE_EXT = '.txt';
@@ -47,8 +47,15 @@ function importFromFile({fileTypeFilter, file} = {}) {
function importFromString(jsonString) {
- const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || [];
- const oldStyles = json.length && deepCopyStyles();
+ if (!BG) {
+ onBackgroundReady().then(() => importFromString(jsonString));
+ return;
+ }
+ const json = BG.tryJSONparse(jsonString) || []; // create object in background context
+ if (typeof json.slice != 'function') {
+ json.length = 0;
+ }
+ const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
const oldStylesByName = json.length && new Map(
oldStyles.map(style => [style.name.trim(), style]));
const stats = {
@@ -60,18 +67,19 @@ function importFromString(jsonString) {
invalid: {names: [], legend: 'invalid skipped'},
};
let index = 0;
+ let lastRepaint = performance.now();
return new Promise(proceed);
function proceed(resolve) {
while (index < json.length) {
const item = json[index++];
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
- || (item.sections && !(item.sections instanceof Array))) {
+ || (item.sections && typeof item.sections.slice != 'function')) {
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
continue;
}
item.name = item.name.trim();
- const byId = cachedStyles.byId.get(item.id);
+ const byId = BG.cachedStyles.byId.get(item.id);
const byName = oldStylesByName.get(item.name);
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
if (oldStyle == byName && byName) {
@@ -81,16 +89,22 @@ function importFromString(jsonString) {
const metaEqual = oldStyleKeys &&
oldStyleKeys.length == Object.keys(item).length &&
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
- const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item);
+ const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
if (metaEqual && codeEqual) {
stats.unchanged.names.push(oldStyle.name);
stats.unchanged.ids.push(oldStyle.id);
continue;
}
- saveStyle(Object.assign(item, {
+ // using saveStyle directly since json was parsed in background page context
+ BG.saveStyle(Object.assign(item, {
reason: 'import',
notify: false,
})).then(style => {
+ handleUpdate(style, {reason: 'import'});
+ if (performance.now() - lastRepaint > 1000) {
+ scrollElementIntoView($('#style-' + style.id));
+ lastRepaint = performance.now();
+ }
setTimeout(proceed, 0, resolve);
if (!oldStyle) {
stats.added.names.push(style.name);
@@ -120,17 +134,22 @@ function importFromString(jsonString) {
stats.metaOnly.names.length +
stats.codeOnly.names.length +
stats.added.names.length;
- Promise.resolve(numChanged && refreshAllTabs()).then(() => {
- scrollTo(0, 0);
+ Promise.resolve(numChanged && BG.refreshAllTabs()).then(() => {
+ const listNames = kind => {
+ const {ids, names} = stats[kind];
+ return ids
+ ? names.map((name, i) => `
${name}
`)
+ : names.map(name => `${name}
`);
+ };
const report = Object.keys(stats)
.filter(kind => stats[kind].names.length)
- .map(kind => `
+ .map(kind =>
+ `
${stats[kind].names.length} ${stats[kind].legend}
- ` + stats[kind].names.map((name, i) =>
- `${name}
`).join('') + `
-
+ ${listNames(kind).join('')}
`)
.join('');
+ scrollTo(0, 0);
messageBox({
title: 'Finished importing styles',
contents: report || 'Nothing was changed.',
@@ -155,7 +174,7 @@ function importFromString(jsonString) {
];
index = 0;
return new Promise(undoNextId)
- .then(refreshAllTabs)
+ .then(BG.refreshAllTabs)
.then(() => messageBox({
title: 'Import has been undone',
contents: newIds.length + ' styles were reverted.',
@@ -167,14 +186,14 @@ function importFromString(jsonString) {
return;
}
const id = newIds[index++];
- deleteStyle(id, {notify: false}).then(id => {
+ deleteStyleSafe({id, notify: false}).then(id => {
const oldStyle = oldStylesById.get(id);
if (oldStyle) {
- saveStyle(Object.assign(oldStyle, {
- reason: 'undoImport',
+ saveStyleSafe(Object.assign(oldStyle, {
+ reason: 'import',
notify: false,
- }))
- .then(() => setTimeout(undoNextId, 0, resolve));
+ })).then(() =>
+ setTimeout(undoNextId, 0, resolve));
} else {
setTimeout(undoNextId, 0, resolve);
}
@@ -198,25 +217,6 @@ function importFromString(jsonString) {
}
}
- function deepCopyStyles() {
- const clonedStyles = [];
- for (let style of cachedStyles.list || []) {
- style = Object.assign({}, style);
- style.sections = style.sections.slice();
- for (let i = 0, section; (section = style.sections[i]); i++) {
- const copy = style.sections[i] = Object.assign({}, section);
- for (const propName in copy) {
- const prop = copy[propName];
- if (prop instanceof Array) {
- copy[propName] = prop.slice();
- }
- }
- }
- clonedStyles.push(style);
- }
- return clonedStyles;
- }
-
function limitString(s, limit = 100) {
return s.length <= limit ? s : s.substr(0, limit) + '...';
}
diff --git a/dom.js b/dom.js
index 2214fa82..c41f3bcc 100644
--- a/dom.js
+++ b/dom.js
@@ -50,6 +50,21 @@ function animateElement(element, {className, remove = false}) {
}
+function enforceInputRange(element) {
+ const min = Number(element.min);
+ const max = Number(element.max);
+ const onChange = () => {
+ const value = Number(element.value);
+ if (value < min || value > max) {
+ element.value = Math.max(min, Math.min(max, value));
+ }
+ };
+ onChange();
+ element.addEventListener('change', onChange);
+ element.addEventListener('input', onChange);
+}
+
+
function $(selector, base = document) {
// we have ids with . like #manage.onlyEdited which look like #id.class
// so since getElementById is superfast we'll try it anyway
diff --git a/edit.html b/edit.html
index 9deb6bf2..6a3597df 100644
--- a/edit.html
+++ b/edit.html
@@ -645,8 +645,8 @@
-
+
diff --git a/edit.js b/edit.js
index 447fe943..fe3644fd 100644
--- a/edit.js
+++ b/edit.js
@@ -252,7 +252,8 @@ function initCodeMirror() {
} else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
- getCodeMirrorThemes(function(themes) {
+ BG.getCodeMirrorThemes().then(themes => {
+ BG.codeMirrorThemes = themes;
themeControl.innerHTML = optionsHtmlFromArray(themes);
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
});
@@ -1333,7 +1334,7 @@ function save() {
}
var name = document.getElementById("name").value;
var enabled = document.getElementById("enabled").checked;
- saveStyle({
+ saveStyleSafe({
id: styleId,
name: name,
enabled: enabled,
@@ -1815,7 +1816,9 @@ function getParams() {
return params;
}
-chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
+chrome.runtime.onMessage.addListener(onRuntimeMessage);
+
+function onRuntimeMessage(request) {
switch (request.method) {
case "styleUpdated":
if (styleId && styleId == request.style.id && request.reason != 'editSave') {
@@ -1838,7 +1841,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
document.execCommand('delete');
break;
}
-});
+}
function getComputedHeight(el) {
var compStyle = getComputedStyle(el);
diff --git a/manage.html b/manage.html
index a887aa9e..32b8b92a 100644
--- a/manage.html
+++ b/manage.html
@@ -116,9 +116,8 @@
-
-
+
diff --git a/manage.js b/manage.js
index fc44e139..657cfa4a 100644
--- a/manage.js
+++ b/manage.js
@@ -27,7 +27,9 @@ Promise.all([
});
-chrome.runtime.onMessage.addListener(msg => {
+chrome.runtime.onMessage.addListener(onRuntimeMessage);
+
+function onRuntimeMessage(msg) {
switch (msg.method) {
case 'styleUpdated':
case 'styleAdded':
@@ -37,7 +39,7 @@ chrome.runtime.onMessage.addListener(msg => {
handleDelete(msg.id);
break;
}
-});
+}
function initGlobalEvents() {
@@ -151,8 +153,6 @@ function createStyleElement({style, name}) {
(style.enabled ? 'enabled' : 'disabled') +
(style.updateUrl ? ' updatable' : ''),
id: 'style-' + style.id,
- styleId: style.id,
- styleNameLowerCase: name || style.name.toLocaleLowerCase(),
});
parts.nameLink.textContent = style.name;
@@ -216,6 +216,8 @@ function createStyleElement({style, name}) {
}
const newEntry = parts.entry.cloneNode(true);
+ newEntry.styleId = style.id;
+ newEntry.styleNameLowerCase = name || style.name.toLocaleLowerCase();
const newTargets = $('.targets', newEntry);
if (numTargets) {
newTargets.parentElement.replaceChild(targets, newTargets);
@@ -282,7 +284,10 @@ Object.assign(handleEvent, {
},
toggle(event, entry) {
- enableStyle(entry.styleId, this.matches('.enable') || this.checked);
+ saveStyleSafe({
+ id: entry.styleId,
+ enabled: this.matches('.enable') || this.checked,
+ });
},
check(event, entry) {
@@ -291,7 +296,7 @@ Object.assign(handleEvent, {
update(event, entry) {
// update everything but name
- saveStyle(Object.assign(entry.updatedCode, {
+ saveStyleSafe(Object.assign(entry.updatedCode, {
id: entry.styleId,
name: null,
reason: 'update',
@@ -300,7 +305,7 @@ Object.assign(handleEvent, {
delete(event, entry) {
const id = entry.styleId;
- const {name} = cachedStyles.byId.get(id) || {};
+ const {name} = BG.cachedStyles.byId.get(id) || {};
animateElement(entry, {className: 'highlight'});
messageBox({
title: t('deleteStyleConfirm'),
@@ -310,7 +315,7 @@ Object.assign(handleEvent, {
})
.then(({button, enter, esc}) => {
if (button == 0 || enter) {
- deleteStyle(id);
+ deleteStyleSafe({id});
}
});
},
@@ -335,7 +340,7 @@ Object.assign(handleEvent, {
});
-function handleUpdate(style, {reason, quiet} = {}) {
+function handleUpdate(style, {reason} = {}) {
const element = createStyleElement({style});
const oldElement = $('#style-' + style.id, installed);
if (oldElement) {
@@ -354,8 +359,8 @@ function handleUpdate(style, {reason, quiet} = {}) {
installed.insertBefore(element, findNextElement(style));
if (reason != 'import') {
animateElement(element, {className: 'highlight'});
+ scrollElementIntoView(element);
}
- scrollElementIntoView(element);
}
@@ -465,7 +470,7 @@ function checkUpdate(element) {
class Updater {
constructor(element) {
- const style = cachedStyles.byId.get(element.styleId);
+ const style = BG.cachedStyles.byId.get(element.styleId);
Object.assign(this, {
element,
id: style.id,
@@ -504,7 +509,7 @@ class Updater {
handleJson(forceUpdate, json) {
return getStylesSafe({id: this.id}).then(([style]) => {
- const needsUpdate = forceUpdate || !styleSectionsEqual(style, json);
+ const needsUpdate = forceUpdate || !BG.styleSectionsEqual(style, json);
this.display({json: needsUpdate && json});
return needsUpdate;
});
@@ -598,7 +603,7 @@ function searchStyles({immediately, container}) {
}
for (const element of (container || installed).children) {
- const style = cachedStyles.byId.get(element.styleId) || {};
+ const style = BG.cachedStyles.byId.get(element.styleId) || {};
if (style) {
const isMatching = !query
|| isMatchingText(style.name)
diff --git a/manifest.json b/manifest.json
index 929b70dd..0628b51a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -19,7 +19,7 @@
"*://*/*"
],
"background": {
- "scripts": ["messaging.js", "storage.js", "background.js", "update.js"]
+ "scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"]
},
"commands": {
"openManage": {
diff --git a/messaging.js b/messaging.js
index b62c9673..3975b777 100644
--- a/messaging.js
+++ b/messaging.js
@@ -1,4 +1,4 @@
-/* global getStyleWithNoCode, applyOnMessage, onBackgroundMessage, getStyles */
+/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
'use strict';
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
@@ -7,134 +7,59 @@ const FIREFOX = /Firefox/.test(navigator.userAgent);
const OPERA = /OPR/.test(navigator.userAgent);
const URLS = {
ownOrigin: chrome.runtime.getURL(''),
- optionsUI: new Set([
+ optionsUI: [
chrome.runtime.getURL('options/index.html'),
'chrome://extensions/?options=' + chrome.runtime.id,
- ]),
- configureCommands: OPERA ? 'opera://settings/configureCommands'
- : 'chrome://extensions/configureCommands',
+ ],
+ configureCommands:
+ OPERA ? 'opera://settings/configureCommands'
+ : 'chrome://extensions/configureCommands',
};
const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${URLS.ownOrigin}`);
-document.documentElement.classList.toggle('firefox', FIREFOX);
-document.documentElement.classList.toggle('opera', OPERA);
+let BG = chrome.extension.getBackgroundPage();
+if (!BG || BG != window) {
+ document.documentElement.classList.toggle('firefox', FIREFOX);
+ document.documentElement.classList.toggle('opera', OPERA);
+}
-function notifyAllTabs(request) {
- // list all tabs including chrome-extension:// which can be ours
- if (request.codeIsUpdated === false && request.style) {
- request = Object.assign({}, request, {
- style: getStyleWithNoCode(request.style)
+function notifyAllTabs(msg) {
+ const originalMessage = msg;
+ if (msg.codeIsUpdated === false && msg.style) {
+ msg = Object.assign({}, msg, {
+ style: getStyleWithNoCode(msg.style)
});
}
- const affectsAll = !request.affects || request.affects.all;
- const affectsOwnOrigin = !affectsAll && (request.affects.editor || request.affects.manager);
+ const affectsAll = !msg.affects || msg.affects.all;
+ const affectsOwnOrigin = !affectsAll && (msg.affects.editor || msg.affects.manager);
const affectsTabs = affectsAll || affectsOwnOrigin;
- const affectsIcon = affectsAll || request.affects.icon;
- const affectsPopup = affectsAll || request.affects.popup;
+ const affectsIcon = affectsAll || msg.affects.icon;
+ const affectsPopup = affectsAll || msg.affects.popup;
if (affectsTabs || affectsIcon) {
+ // list all tabs including chrome-extension:// which can be ours
chrome.tabs.query(affectsOwnOrigin ? {url: URLS.ownOrigin + '*'} : {}, tabs => {
for (const tab of tabs) {
- if (affectsTabs || URLS.optionsUI.has(tab.url)) {
- chrome.tabs.sendMessage(tab.id, request);
+ if (affectsTabs || URLS.optionsUI.includes(tab.url)) {
+ chrome.tabs.sendMessage(tab.id, msg);
}
- if (affectsIcon) {
- updateIcon(tab);
+ if (affectsIcon && BG) {
+ BG.updateIcon(tab);
}
}
});
}
// notify self: the message no longer is sent to the origin in new Chrome
- if (window.applyOnMessage) {
- applyOnMessage(request);
- } else if (window.onBackgroundMessage) {
- onBackgroundMessage(request);
+ if (typeof onRuntimeMessage != 'undefined') {
+ onRuntimeMessage(originalMessage);
+ }
+ // notify apply.js on own pages
+ if (typeof applyOnMessage != 'undefined') {
+ applyOnMessage(originalMessage);
}
// notify background page and all open popups
- if (affectsPopup || request.prefs) {
- chrome.runtime.sendMessage(request);
- }
-}
-
-
-function refreshAllTabs() {
- return new Promise(resolve => {
- // list all tabs including chrome-extension:// which can be ours
- chrome.tabs.query({}, tabs => {
- const lastTab = tabs[tabs.length - 1];
- for (const tab of tabs) {
- getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => {
- const message = {method: 'styleReplaceAll', styles};
- if (tab.url == location.href && typeof applyOnMessage !== 'undefined') {
- applyOnMessage(message);
- } else {
- chrome.tabs.sendMessage(tab.id, message);
- }
- updateIcon(tab, styles);
- if (tab == lastTab) {
- resolve();
- }
- });
- }
- });
- });
-}
-
-
-function updateIcon(tab, styles) {
- // while NTP is still loading only process the request for its main frame with a real url
- // (but when it's loaded we should process style toggle requests from popups, for example)
- const isNTP = tab.url == 'chrome://newtab/';
- if (isNTP && tab.status != 'complete' || tab.id < 0) {
- return;
- }
- if (styles) {
- // check for not-yet-existing tabs e.g. omnibox instant search
- chrome.tabs.get(tab.id, () => {
- if (!chrome.runtime.lastError) {
- stylesReceived(styles);
- }
- });
- return;
- }
- if (isNTP) {
- getTabRealURL(tab).then(url =>
- getStyles({matchUrl: url, enabled: true, asHash: true}, stylesReceived));
- } else {
- getStyles({matchUrl: tab.url, enabled: true, asHash: true}, stylesReceived);
- }
-
- function stylesReceived(styles) {
- let numStyles = styles.length;
- if (numStyles === undefined) {
- // for 'styles' asHash:true fake the length by counting numeric ids manually
- numStyles = 0;
- for (const id of Object.keys(styles)) {
- numStyles += id.match(/^\d+$/) ? 1 : 0;
- }
- }
- const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
- const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : '';
- const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
- const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
- chrome.browserAction.setIcon({
- tabId: tab.id,
- path: {
- // Material Design 2016 new size is 16px
- 16: `images/icon/16${postfix}.png`,
- 32: `images/icon/32${postfix}.png`,
- // Chromium forks or non-chromium browsers may still use the traditional 19px
- 19: `images/icon/19${postfix}.png`,
- 38: `images/icon/38${postfix}.png`,
- // TODO: add Edge preferred sizes: 20, 25, 30, 40
- },
- }, () => {
- if (!chrome.runtime.lastError) {
- // Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
- chrome.browserAction.setBadgeBackgroundColor({color});
- chrome.browserAction.setBadgeText({text, tabId: tab.id});
- }
- });
+ if (affectsPopup || msg.prefs) {
+ chrome.runtime.sendMessage(msg);
}
}
@@ -211,12 +136,153 @@ function stringAsRegExp(s, flags) {
}
-// expands * as .*?
-function wildcardAsRegExp(s, flags) {
- return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags);
-}
-
-
function ignoreChromeError() {
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
}
+
+
+function getStyleWithNoCode(style) {
+ const stripped = Object.assign({}, style, {sections: []});
+ for (const section of style.sections) {
+ stripped.sections.push(Object.assign({}, section, {code: null}));
+ }
+ return stripped;
+}
+
+
+// js engine can't optimize the entire function if it contains try-catch
+// so we should keep it isolated from normal code in a minimal wrapper
+// Update: might get fixed in V8 TurboFan in the future
+function tryCatch(func, ...args) {
+ try {
+ return func(...args);
+ } catch (e) {}
+}
+
+
+function tryRegExp(regexp) {
+ try {
+ return new RegExp(regexp);
+ } catch (e) {}
+}
+
+
+function tryJSONparse(jsonString) {
+ try {
+ return JSON.parse(jsonString);
+ } catch (e) {}
+}
+
+
+function debounce(fn, delay, ...args) {
+ const timers = debounce.timers = debounce.timers || new Map();
+ debounce.run = debounce.run || ((fn, ...args) => {
+ timers.delete(fn);
+ fn(...args);
+ });
+ clearTimeout(timers.get(fn));
+ timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
+}
+
+
+function deepCopy(obj) {
+ if (!obj || typeof obj != 'object') {
+ return obj;
+ } else {
+ const emptyCopy = Object.create(Object.getPrototypeOf(obj));
+ return deepMerge(emptyCopy, obj);
+ }
+}
+
+
+function deepMerge(target, ...args) {
+ for (const obj of args) {
+ for (const k in obj) {
+ const value = obj[k];
+ if (!value || typeof value != 'object') {
+ target[k] = value;
+ } else if (typeof value.slice == 'function') {
+ const arrayCopy = target[k] = target[k] || [];
+ for (const element of value) {
+ arrayCopy.push(deepCopy(element));
+ }
+ } else if (k in target) {
+ deepMerge(target[k], value);
+ } else {
+ target[k] = deepCopy(value);
+ }
+ }
+ }
+ return target;
+}
+
+
+function sessionStorageHash(name) {
+ return {
+ name,
+ value: tryCatch(JSON.parse, sessionStorage[name]) || {},
+ set(k, v) {
+ this.value[k] = v;
+ this.updateStorage();
+ },
+ unset(k) {
+ delete this.value[k];
+ this.updateStorage();
+ },
+ updateStorage() {
+ sessionStorage[this.name] = JSON.stringify(this.value);
+ }
+ };
+}
+
+
+function onBackgroundReady() {
+ return BG ? Promise.resolve() : new Promise(ping);
+ function ping(resolve) {
+ chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
+ if (health !== undefined) {
+ BG = chrome.extension.getBackgroundPage();
+ resolve();
+ } else {
+ ping(resolve);
+ }
+ });
+ }
+}
+
+
+// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
+function getStylesSafe(options) {
+ return new Promise(resolve => {
+ if (BG) {
+ BG.getStyles(options, resolve);
+ } else {
+ onBackgroundReady().then(() =>
+ BG.getStyles(options, resolve));
+ }
+ });
+}
+
+
+function saveStyleSafe(style) {
+ return onBackgroundReady()
+ .then(() => BG.saveStyle(BG.deepCopy(style)))
+ .then(savedStyle => {
+ if (style.notify === false) {
+ handleUpdate(savedStyle, style);
+ }
+ return savedStyle;
+ });
+}
+
+
+function deleteStyleSafe({id, notify = true} = {}) {
+ return onBackgroundReady()
+ .then(() => BG.deleteStyle({id, notify}))
+ .then(() => {
+ if (!notify) {
+ handleDelete(id);
+ }
+ return id;
+ });
+}
diff --git a/options/index.html b/options/index.html
index 98978a87..b98c447b 100644
--- a/options/index.html
+++ b/options/index.html
@@ -5,9 +5,9 @@
-
-
+
+
diff --git a/options/index.js b/options/index.js
index f3e52cfa..19ecf930 100644
--- a/options/index.js
+++ b/options/index.js
@@ -1,7 +1,5 @@
-/* global update */
'use strict';
-
setupLivePrefs([
'show-badge',
'popup.stylesFirst',
@@ -33,7 +31,7 @@ document.onclick = e => {
}
function check() {
- chrome.extension.getBackgroundPage().update.perform((cmd, value) => {
+ BG.update.perform((cmd, value) => {
switch (cmd) {
case 'count':
total = value;
diff --git a/popup.html b/popup.html
index ecb152f5..1c026e9e 100644
--- a/popup.html
+++ b/popup.html
@@ -57,9 +57,8 @@
-
-
+
diff --git a/popup.js b/popup.js
index bacb1316..aed550b7 100644
--- a/popup.js
+++ b/popup.js
@@ -1,4 +1,3 @@
-/* global SLOPPY_REGEXP_PREFIX, compileStyleRegExps */
'use strict';
let installed;
@@ -17,8 +16,9 @@ getActiveTabRealURL().then(url => {
});
});
+chrome.runtime.onMessage.addListener(onRuntimeMessage);
-chrome.runtime.onMessage.addListener(msg => {
+function onRuntimeMessage(msg) {
switch (msg.method) {
case 'styleAdded':
case 'styleUpdated':
@@ -38,7 +38,7 @@ chrome.runtime.onMessage.addListener(msg => {
}
break;
}
-});
+}
function setPopupWidth(width = prefs.get('popupWidth')) {
@@ -117,7 +117,7 @@ function initPopup(url) {
matchTargets.appendChild(urlLink);
// For domain
- const domains = getDomains(url);
+ const domains = BG.getDomains(url);
for (const domain of domains) {
// Don't include TLD
if (domains.length > 1 && !domain.includes('.')) {
@@ -252,7 +252,7 @@ Object.assign(handleEvent, {
},
toggle(event) {
- saveStyle({
+ saveStyleSafe({
id: handleEvent.getClickedStyleId(event),
enabled: this.type == 'checkbox' ? this.checked : this.matches('.enable'),
});
@@ -263,7 +263,7 @@ Object.assign(handleEvent, {
const box = $('#confirm');
box.dataset.display = true;
box.style.cssText = '';
- $('b', box).textContent = (cachedStyles.byId.get(id) || {}).name;
+ $('b', box).textContent = (BG.cachedStyles.byId.get(id) || {}).name;
$('[data-cmd="ok"]', box).onclick = () => confirm(true);
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
window.onkeydown = event => {
@@ -278,7 +278,7 @@ Object.assign(handleEvent, {
animateElement(box, {className: 'lights-on'})
.then(() => (box.dataset.display = false));
if (ok) {
- deleteStyle(id).then(() => {
+ deleteStyleSafe({id}).then(() => {
// update view with 'No styles installed for this site' message
if (!installed.children.length) {
showStyles([]);
@@ -297,7 +297,7 @@ Object.assign(handleEvent, {
entry.appendChild(info);
},
- closeExplanation(event) {
+ closeExplanation() {
$('#regexp-explanation').remove();
},
@@ -347,7 +347,7 @@ function handleUpdate(style) {
return;
}
// Add an entry when a new style for the current url is installed
- if (tabURL && getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
+ if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
$('#unavailable').style.display = 'none';
createStyleElement({style});
}
@@ -368,13 +368,15 @@ function handleDelete(id) {
*/
function detectSloppyRegexps({entry, style}) {
const {
- appliedSections = getApplicableSections({style, matchUrl: tabURL}),
- wannabeSections = getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
+ appliedSections =
+ BG.getApplicableSections({style, matchUrl: tabURL}),
+ wannabeSections =
+ BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
} = style;
- compileStyleRegExps({style, compileAll: true});
+ BG.compileStyleRegExps({style, compileAll: true});
entry.hasInvalidRegexps = wannabeSections.some(section =>
- section.regexps.some(rx => !cachedStyles.regexps.has(rx)));
+ section.regexps.some(rx => !BG.cachedStyles.regexps.has(rx)));
entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
if (!appliedSections.length) {
diff --git a/prefs.js b/prefs.js
new file mode 100644
index 00000000..a04e1bee
--- /dev/null
+++ b/prefs.js
@@ -0,0 +1,265 @@
+/* global prefs: true, contextMenus */
+'use strict';
+
+// eslint-disable-next-line no-var
+var prefs = new function Prefs() {
+ const defaults = {
+ 'openEditInWindow': false, // new editor opens in a own browser window
+ 'windowPosition': {}, // detached window position
+ 'show-badge': true, // display text on popup menu icon
+ 'disableAll': false, // boss key
+
+ 'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
+ 'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
+ 'popup.enabledFirst': true, // display enabled styles before disabled styles
+ 'popup.stylesFirst': true, // display enabled styles before disabled styles
+
+ 'manage.onlyEnabled': false, // display only enabled styles
+ 'manage.onlyEdited': false, // display only styles created locally
+ 'manage.newUI': true, // use the new compact layout
+ 'manage.newUI.favicons': true, // show favicons for the sites in applies-to
+ 'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
+
+ 'editor.options': {}, // CodeMirror.defaults.*
+ 'editor.lineWrapping': true, // word wrap
+ 'editor.smartIndent': true, // 'smart' indent
+ 'editor.indentWithTabs': false, // smart indent with tabs
+ 'editor.tabSize': 4, // tab width, in spaces
+ 'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default',
+ 'editor.theme': 'default', // CSS theme
+ 'editor.beautify': { // CSS beautifier
+ selector_separator_newline: true,
+ newline_before_open_brace: false,
+ newline_after_open_brace: true,
+ newline_between_properties: true,
+ newline_before_close_brace: true,
+ newline_between_rules: false,
+ end_with_newline: false,
+ space_around_selector_separator: true,
+ },
+ 'editor.lintDelay': 500, // lint gutter marker update delay, ms
+ 'editor.lintReportDelay': 4500, // lint report update delay, ms
+ 'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected
+ // selection = only when something is selected
+ // '' (empty string) = disabled
+
+ 'badgeDisabled': '#8B0000', // badge background color when disabled
+ 'badgeNormal': '#006666', // badge background color
+
+ 'popupWidth': 246, // popup width in pixels
+
+ 'updateInterval': 0 // user-style automatic update interval, hour
+ };
+ const values = deepCopy(defaults);
+
+ const affectsIcon = [
+ 'show-badge',
+ 'disableAll',
+ 'badgeDisabled',
+ 'badgeNormal',
+ ];
+
+ // coalesce multiple pref changes in broadcast
+ let broadcastPrefs = {};
+
+ Object.defineProperty(this, 'readOnlyValues', {value: {}});
+
+ Object.assign(Prefs.prototype, {
+
+ get(key, defaultValue) {
+ if (key in values) {
+ return values[key];
+ }
+ if (defaultValue !== undefined) {
+ return defaultValue;
+ }
+ if (key in defaults) {
+ return defaults[key];
+ }
+ console.warn("No default preference for '%s'", key);
+ },
+
+ getAll() {
+ return deepCopy(values);
+ },
+
+ set(key, value, {noBroadcast, noSync} = {}) {
+ const oldValue = deepCopy(values[key]);
+ values[key] = value;
+ defineReadonlyProperty(this.readOnlyValues, key, value);
+ if (!noBroadcast && !equal(value, oldValue)) {
+ this.broadcast(key, value, {noSync});
+ }
+ localStorage[key] = typeof defaults[key] == 'object'
+ ? JSON.stringify(value)
+ : value;
+ },
+
+ remove: key => this.set(key, undefined),
+
+ reset: key => this.set(key, deepCopy(defaults[key])),
+
+ broadcast(key, value, {noSync} = {}) {
+ broadcastPrefs[key] = value;
+ debounce(doBroadcast);
+ if (!noSync) {
+ debounce(doSyncSet);
+ }
+ },
+ });
+
+ // Unlike sync, HTML5 localStorage is ready at browser startup
+ // so we'll mirror the prefs to avoid using the wrong defaults
+ // during the startup phase
+ for (const key in defaults) {
+ const defaultValue = defaults[key];
+ let value = localStorage[key];
+ if (typeof value == 'string') {
+ switch (typeof defaultValue) {
+ case 'boolean':
+ value = value.toLowerCase() === 'true';
+ break;
+ case 'number':
+ value |= 0;
+ break;
+ case 'object':
+ value = tryJSONparse(value) || defaultValue;
+ break;
+ }
+ } else {
+ value = defaultValue;
+ }
+ this.set(key, value, {noBroadcast: true});
+ }
+
+ getSync().get('settings', ({settings: synced} = {}) => {
+ if (synced) {
+ for (const key in defaults) {
+ if (key == 'popupWidth' && synced[key] != values.popupWidth) {
+ // this is a fix for the period when popupWidth wasn't synced
+ // TODO: remove it in a couple of months
+ continue;
+ }
+ if (key in synced) {
+ this.set(key, synced[key], {noSync: true});
+ }
+ }
+ }
+ if (typeof contextMenus !== 'undefined') {
+ for (const id in contextMenus) {
+ if (typeof values[id] == 'boolean') {
+ this.broadcast(id, values[id], {noSync: true});
+ }
+ }
+ }
+ });
+
+ chrome.storage.onChanged.addListener((changes, area) => {
+ if (area == 'sync' && 'settings' in changes) {
+ const synced = changes.settings.newValue;
+ if (synced) {
+ for (const key in defaults) {
+ if (key in synced) {
+ this.set(key, synced[key], {noSync: true});
+ }
+ }
+ } else {
+ // user manually deleted our settings, we'll recreate them
+ getSync().set({'settings': values});
+ }
+ }
+ });
+
+ function doBroadcast() {
+ const affects = {all: 'disableAll' in broadcastPrefs};
+ if (!affects.all) {
+ for (const key in broadcastPrefs) {
+ affects.icon = affects.icon || affectsIcon.includes(key);
+ affects.popup = affects.popup || key.startsWith('popup');
+ affects.editor = affects.editor || key.startsWith('editor');
+ affects.manager = affects.manager || key.startsWith('manage');
+ }
+ }
+ notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
+ broadcastPrefs = {};
+ }
+
+ function doSyncSet() {
+ getSync().set({'settings': values});
+ }
+
+ // Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
+ function getSync() {
+ if ('sync' in chrome.storage) {
+ return chrome.storage.sync;
+ }
+ const crappyStorage = {};
+ return {
+ get(key, callback) {
+ callback(crappyStorage[key] || {});
+ },
+ set(source, callback) {
+ for (const property in source) {
+ if (source.hasOwnProperty(property)) {
+ crappyStorage[property] = source[property];
+ }
+ }
+ callback();
+ }
+ };
+ }
+
+ function defineReadonlyProperty(obj, key, value) {
+ const copy = deepCopy(value);
+ if (typeof copy == 'object') {
+ Object.freeze(copy);
+ }
+ Object.defineProperty(obj, key, {value: copy, configurable: true});
+ }
+
+ function equal(a, b) {
+ if (!a || !b || typeof a != 'object' || typeof b != 'object') {
+ return a === b;
+ }
+ if (Object.keys(a).length != Object.keys(b).length) {
+ return false;
+ }
+ for (const k in a) {
+ if (a[k] !== b[k]) {
+ return false;
+ }
+ }
+ return true;
+ }
+}();
+
+
+// Accepts an array of pref names (values are fetched via prefs.get)
+// and establishes a two-way connection between the document elements and the actual prefs
+function setupLivePrefs(IDs) {
+ const localIDs = {};
+ IDs.forEach(function(id) {
+ localIDs[id] = true;
+ updateElement(id).addEventListener('change', function() {
+ prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
+ });
+ });
+ chrome.runtime.onMessage.addListener(msg => {
+ if (msg.prefs) {
+ for (const prefName in msg.prefs) {
+ if (prefName in localIDs) {
+ updateElement(prefName, msg.prefs[prefName]);
+ }
+ }
+ }
+ });
+ function updateElement(id, value) {
+ const el = document.getElementById(id);
+ el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id);
+ el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
+ return el;
+ }
+ function isCheckbox(el) {
+ return el.localName == 'input' && el.type == 'checkbox';
+ }
+}
diff --git a/storage.js b/storage.js
index f1846fa0..b4bc03bb 100644
--- a/storage.js
+++ b/storage.js
@@ -1,7 +1,28 @@
-/* global cachedStyles: true, prefs: true, contextMenus: false */
-/* global handleUpdate, handleDelete */
+/* global cachedStyles: true */
'use strict';
+const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
+ /(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
+ /[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
+const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
+const SLOPPY_REGEXP_PREFIX = '\0';
+
+// Note, only 'var'-declared variables are visible from another extension page
+// eslint-disable-next-line no-var
+var cachedStyles = {
+ list: null,
+ byId: new Map(),
+ filters: new Map(),
+ regexps: new Map(),
+ urlDomains: new Map(),
+ emptyCode: new Map(), // entire code is comments/whitespace/@namespace
+ mutex: {
+ inProgress: false,
+ onDone: [],
+ },
+};
+
+
function getDatabase(ready, error) {
const dbOpenRequest = window.indexedDB.open('stylish', 2);
dbOpenRequest.onsuccess = event => {
@@ -24,54 +45,6 @@ function getDatabase(ready, error) {
}
-const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
- /(@namespace[\s\r\n]+(?:[^\s\r\n]+[\s\r\n]+)?url\(http:\/\/.*?\);)/,
- /[\s\r\n]*/].map(rx => rx.source).join(''), 'g');
-const RX_CSS_COMMENTS = /\/\*[\s\S]*?\*\//g;
-const SLOPPY_REGEXP_PREFIX = '\0';
-
-// Let manage/popup/edit reuse background page variables
-// Note, only 'var'-declared variables are visible from another extension page
-// eslint-disable-next-line no-var
-var cachedStyles, prefs;
-(() => {
- const bg = chrome.extension.getBackgroundPage();
- cachedStyles = bg && bg.cachedStyles || {
- bg,
- list: null,
- byId: new Map(),
- filters: new Map(),
- regexps: new Map(),
- urlDomains: new Map(),
- emptyCode: new Map(), // entire code is comments/whitespace/@namespace
- mutex: {
- inProgress: false,
- onDone: [],
- },
- };
- prefs = bg && bg.prefs;
-})();
-
-
-// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
-function getStylesSafe(options) {
- return new Promise(resolve => {
- if (cachedStyles.bg) {
- getStyles(options, resolve);
- return;
- }
- chrome.runtime.sendMessage(Object.assign({method: 'getStyles'}, options), styles => {
- if (!styles) {
- resolve(getStylesSafe(options));
- } else {
- cachedStyles = chrome.extension.getBackgroundPage().cachedStyles;
- resolve(styles);
- }
- });
- });
-}
-
-
function getStyles(options, callback) {
if (cachedStyles.list) {
callback(filterStyles(options));
@@ -107,60 +80,6 @@ function getStyles(options, callback) {
}
-function getStyleWithNoCode(style) {
- const stripped = Object.assign({}, style, {sections: []});
- for (const section of style.sections) {
- stripped.sections.push(Object.assign({}, section, {code: null}));
- }
- return stripped;
-}
-
-
-function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
- // prevent double-add on echoed invalidation
- const cached = added && cachedStyles.byId.get(added.id);
- if (cached) {
- return;
- }
- if (andNotify) {
- chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId});
- }
- if (!cachedStyles.list) {
- return;
- }
- if (updated) {
- const cached = cachedStyles.byId.get(updated.id);
- if (cached) {
- Object.assign(cached, updated);
- //console.debug('cache: updated', updated);
- }
- cachedStyles.filters.clear();
- return;
- }
- if (added) {
- cachedStyles.list.push(added);
- cachedStyles.byId.set(added.id, added);
- //console.debug('cache: added', added);
- cachedStyles.filters.clear();
- return;
- }
- if (deletedId != undefined) {
- const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style;
- if (deletedStyle) {
- const cachedIndex = cachedStyles.list.indexOf(deletedStyle);
- cachedStyles.list.splice(cachedIndex, 1);
- cachedStyles.byId.delete(deletedId);
- //console.debug('cache: deleted', deletedStyle);
- cachedStyles.filters.clear();
- return;
- }
- }
- cachedStyles.list = null;
- //console.debug('cache cleared');
- cachedStyles.filters.clear();
-}
-
-
function filterStyles({
enabled,
url = null,
@@ -174,10 +93,10 @@ function filterStyles({
id = id === null ? null : Number(id);
if (enabled === null
- && url === null
- && id === null
- && matchUrl === null
- && asHash != true) {
+ && url === null
+ && id === null
+ && matchUrl === null
+ && asHash != true) {
//console.debug('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), enabled, id, asHash, strictRegexp, matchUrl); // eslint-disable-line max-len
return cachedStyles.list;
}
@@ -247,36 +166,6 @@ function filterStyles({
}
-function cleanupCachedFilters({force = false} = {}) {
- if (!force) {
- // sliding timer for 1 second
- clearTimeout(cleanupCachedFilters.timeout);
- cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true});
- return;
- }
- const size = cachedStyles.filters.size;
- const oldestHit = cachedStyles.filters.values().next().value.lastHit;
- const now = Date.now();
- const timeSpan = now - oldestHit;
- const recencyWeight = 5 / size;
- const hitWeight = 1 / 4; // we make ~4 hits per URL
- const lastHitWeight = 10;
- // delete the oldest 10%
- [...cachedStyles.filters.entries()]
- .map(([id, v], index) => ({
- id,
- weight:
- index * recencyWeight +
- v.hits * hitWeight +
- (v.lastHit - oldestHit) / timeSpan * lastHitWeight,
- }))
- .sort((a, b) => a.weight - b.weight)
- .slice(0, size / 10 + 1)
- .forEach(({id}) => cachedStyles.filters.delete(id));
- cleanupCachedFilters.timeout = 0;
-}
-
-
function saveStyle(style) {
return new Promise(resolve => {
getDatabase(db => {
@@ -312,9 +201,6 @@ function saveStyle(style) {
style, codeIsUpdated, reason,
});
}
- if (typeof handleUpdate != 'undefined') {
- handleUpdate(style, {reason});
- }
resolve(style);
};
};
@@ -340,9 +226,6 @@ function saveStyle(style) {
if (notify) {
notifyAllTabs({method: 'styleAdded', style, reason});
}
- if (typeof handleUpdate != 'undefined') {
- handleUpdate(style, {reason});
- }
resolve(style);
};
});
@@ -350,24 +233,7 @@ function saveStyle(style) {
}
-function addMissingStyleTargets(style) {
- style.sections = (style.sections || []).map(section =>
- Object.assign({
- urls: [],
- urlPrefixes: [],
- domains: [],
- regexps: [],
- }, section)
- );
-}
-
-
-function enableStyle(id, enabled) {
- return saveStyle({id, enabled});
-}
-
-
-function deleteStyle(id, {notify = true} = {}) {
+function deleteStyle({id, notify = true}) {
return new Promise(resolve =>
getDatabase(db => {
const tx = db.transaction(['styles'], 'readwrite');
@@ -377,61 +243,12 @@ function deleteStyle(id, {notify = true} = {}) {
if (notify) {
notifyAllTabs({method: 'styleDeleted', id});
}
- if (typeof handleDelete != 'undefined') {
- handleDelete(id);
- }
resolve(id);
};
}));
}
-function reportError(...args) {
- for (const arg of args) {
- if ('message' in arg) {
- console.log(arg.message);
- }
- }
-}
-
-
-function fixBoolean(b) {
- if (typeof b != 'undefined') {
- return b != 'false';
- }
- return null;
-}
-
-
-function getDomains(url) {
- if (url.indexOf('file:') == 0) {
- return [];
- }
- let d = /.*?:\/*([^/:]+)/.exec(url)[1];
- const domains = [d];
- while (d.indexOf('.') != -1) {
- d = d.substring(d.indexOf('.') + 1);
- domains.push(d);
- }
- return domains;
-}
-
-
-function getType(o) {
- if (typeof o == 'undefined' || typeof o == 'string') {
- return typeof o;
- }
- // with the persistent cachedStyles the Array reference is usually different
- // so let's check for e.g. type of 'every' which is only present on arrays
- // (in the context of our extension)
- if (o instanceof Array || typeof o.every == 'function') {
- return 'array';
- }
- console.warn('Unsupported type:', o);
- return 'undefined';
-}
-
-
function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirst}) {
//let t0 = 0;
const sections = [];
@@ -518,392 +335,6 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs
}
-function isCheckbox(el) {
- return el.localName == 'input' && el.type == 'checkbox';
-}
-
-
-// js engine can't optimize the entire function if it contains try-catch
-// so we should keep it isolated from normal code in a minimal wrapper
-// Update: might get fixed in V8 TurboFan in the future
-function runTryCatch(func, ...args) {
- try {
- return func(...args);
- } catch (e) {}
-}
-
-
-function tryRegExp(regexp) {
- try {
- return new RegExp(regexp);
- } catch (e) {}
-}
-
-
-function tryJSONparse(jsonString) {
- try {
- return JSON.parse(jsonString);
- } catch (e) {}
-}
-
-
-function debounce(fn, delay, ...args) {
- const timers = debounce.timers = debounce.timers || new Map();
- debounce.run = debounce.run || ((fn, ...args) => {
- timers.delete(fn);
- fn(...args);
- });
- clearTimeout(timers.get(fn));
- timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
-}
-
-
-prefs = prefs || new function Prefs() {
- const defaults = {
- 'openEditInWindow': false, // new editor opens in a own browser window
- 'windowPosition': {}, // detached window position
- 'show-badge': true, // display text on popup menu icon
- 'disableAll': false, // boss key
-
- 'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
- 'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
- 'popup.enabledFirst': true, // display enabled styles before disabled styles
- 'popup.stylesFirst': true, // display enabled styles before disabled styles
-
- 'manage.onlyEnabled': false, // display only enabled styles
- 'manage.onlyEdited': false, // display only styles created locally
- 'manage.newUI': true, // use the new compact layout
- 'manage.newUI.favicons': true, // show favicons for the sites in applies-to
- 'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
-
- 'editor.options': {}, // CodeMirror.defaults.*
- 'editor.lineWrapping': true, // word wrap
- 'editor.smartIndent': true, // 'smart' indent
- 'editor.indentWithTabs': false, // smart indent with tabs
- 'editor.tabSize': 4, // tab width, in spaces
- 'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default',
- 'editor.theme': 'default', // CSS theme
- 'editor.beautify': { // CSS beautifier
- selector_separator_newline: true,
- newline_before_open_brace: false,
- newline_after_open_brace: true,
- newline_between_properties: true,
- newline_before_close_brace: true,
- newline_between_rules: false,
- end_with_newline: false,
- space_around_selector_separator: true,
- },
- 'editor.lintDelay': 500, // lint gutter marker update delay, ms
- 'editor.lintReportDelay': 4500, // lint report update delay, ms
- 'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected
- // selection = only when something is selected
- // '' (empty string) = disabled
-
- 'badgeDisabled': '#8B0000', // badge background color when disabled
- 'badgeNormal': '#006666', // badge background color
-
- 'popupWidth': 246, // popup width in pixels
-
- 'updateInterval': 0 // user-style automatic update interval, hour
- };
- const values = deepCopy(defaults);
-
- const affectsIcon = [
- 'show-badge',
- 'disableAll',
- 'badgeDisabled',
- 'badgeNormal',
- ];
-
- // coalesce multiple pref changes in broadcast
- let broadcastPrefs = {};
-
- function doBroadcast() {
- const affects = {all: 'disableAll' in broadcastPrefs};
- if (!affects.all) {
- for (const key in broadcastPrefs) {
- affects.icon = affects.icon || affectsIcon.includes(key);
- affects.popup = affects.popup || key.startsWith('popup');
- affects.editor = affects.editor || key.startsWith('editor');
- affects.manager = affects.manager || key.startsWith('manage');
- }
- }
- notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
- broadcastPrefs = {};
- }
-
- function doSyncSet() {
- getSync().set({'settings': values});
- }
-
- Object.defineProperty(this, 'readOnlyValues', {value: {}});
-
- Object.assign(Prefs.prototype, {
-
- get(key, defaultValue) {
- if (key in values) {
- return values[key];
- }
- if (defaultValue !== undefined) {
- return defaultValue;
- }
- if (key in defaults) {
- return defaults[key];
- }
- console.warn("No default preference for '%s'", key);
- },
-
- getAll() {
- return deepCopy(values);
- },
-
- set(key, value, {noBroadcast, noSync} = {}) {
- const oldValue = deepCopy(values[key]);
- values[key] = value;
- defineReadonlyProperty(this.readOnlyValues, key, value);
- if (!noBroadcast && !equal(value, oldValue)) {
- this.broadcast(key, value, {noSync});
- }
- localStorage[key] = typeof defaults[key] == 'object'
- ? JSON.stringify(value)
- : value;
- },
-
- remove: key => this.set(key, undefined),
-
- reset: key => this.set(key, deepCopy(defaults[key])),
-
- broadcast(key, value, {noSync} = {}) {
- broadcastPrefs[key] = value;
- debounce(doBroadcast);
- if (!noSync) {
- debounce(doSyncSet);
- }
- },
- });
-
- // Unlike sync, HTML5 localStorage is ready at browser startup
- // so we'll mirror the prefs to avoid using the wrong defaults
- // during the startup phase
- for (const key in defaults) {
- const defaultValue = defaults[key];
- let value = localStorage[key];
- if (typeof value == 'string') {
- switch (typeof defaultValue) {
- case 'boolean':
- value = value.toLowerCase() === 'true';
- break;
- case 'number':
- value |= 0;
- break;
- case 'object':
- value = tryJSONparse(value) || defaultValue;
- break;
- }
- } else {
- value = defaultValue;
- }
- this.set(key, value, {noBroadcast: true});
- }
-
- getSync().get('settings', ({settings: synced}) => {
- if (synced) {
- for (const key in defaults) {
- if (key == 'popupWidth' && synced[key] != values.popupWidth) {
- // this is a fix for the period when popupWidth wasn't synced
- // TODO: remove it in a couple of months before the summer 2017
- continue;
- }
- if (key in synced) {
- this.set(key, synced[key], {noSync: true});
- }
- }
- }
- if (typeof contextMenus !== 'undefined') {
- for (const id in contextMenus) {
- if (typeof values[id] == 'boolean') {
- this.broadcast(id, values[id], {noSync: true});
- }
- }
- }
- });
-
- chrome.storage.onChanged.addListener((changes, area) => {
- if (area == 'sync' && 'settings' in changes) {
- const synced = changes.settings.newValue;
- if (synced) {
- for (const key in defaults) {
- if (key in synced) {
- this.set(key, synced[key], {noSync: true});
- }
- }
- } else {
- // user manually deleted our settings, we'll recreate them
- getSync().set({'settings': values});
- }
- }
- });
-}();
-
-
-// Accepts an array of pref names (values are fetched via prefs.get)
-// and establishes a two-way connection between the document elements and the actual prefs
-function setupLivePrefs(IDs) {
- const localIDs = {};
- IDs.forEach(function(id) {
- localIDs[id] = true;
- updateElement(id).addEventListener('change', function() {
- prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
- });
- });
- chrome.runtime.onMessage.addListener(msg => {
- if (msg.prefs) {
- for (const prefName in msg.prefs) {
- if (prefName in localIDs) {
- updateElement(prefName, msg.prefs[prefName]);
- }
- }
- }
- });
- function updateElement(id, value) {
- const el = document.getElementById(id);
- el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id);
- el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
- return el;
- }
-}
-
-
-function enforceInputRange(element) {
- const min = Number(element.min);
- const max = Number(element.max);
- const onChange = () => {
- const value = Number(element.value);
- if (value < min || value > max) {
- element.value = Math.max(min, Math.min(max, value));
- }
- };
- onChange();
- element.addEventListener('change', onChange);
- element.addEventListener('input', onChange);
-}
-
-
-function getCodeMirrorThemes(callback) {
- chrome.runtime.getPackageDirectoryEntry(function(rootDir) {
- rootDir.getDirectory('codemirror/theme', {create: false}, function(themeDir) {
- themeDir.createReader().readEntries(function(entries) {
- const themes = [chrome.i18n.getMessage('defaultTheme')];
- entries
- .filter(entry => entry.isFile)
- .sort((a, b) => (a.name < b.name ? -1 : 1))
- .forEach(function(entry) {
- themes.push(entry.name.replace(/\.css$/, ''));
- });
- if (callback) {
- callback(themes);
- }
- });
- });
- });
-}
-
-
-function sessionStorageHash(name) {
- return {
- name,
- value: runTryCatch(JSON.parse, sessionStorage[name]) || {},
- set(k, v) {
- this.value[k] = v;
- this.updateStorage();
- },
- unset(k) {
- delete this.value[k];
- this.updateStorage();
- },
- updateStorage() {
- sessionStorage[this.name] = JSON.stringify(this.value);
- }
- };
-}
-
-
-function deepCopy(obj) {
- if (!obj || typeof obj != 'object') {
- return obj;
- } else {
- const emptyCopy = Object.create(Object.getPrototypeOf(obj));
- return deepMerge(emptyCopy, obj);
- }
-}
-
-
-function deepMerge(target, ...args) {
- for (const obj of args) {
- for (const k in obj) {
- const value = obj[k];
- if (!value || typeof value != 'object') {
- target[k] = value;
- } else if (k in target) {
- deepMerge(target[k], value);
- } else if (typeof value.slice == 'function') {
- target[k] = value.slice();
- } else {
- target[k] = deepCopy(value);
- }
- }
- }
- return target;
-}
-
-
-function equal(a, b) {
- if (!a || !b || typeof a != 'object' || typeof b != 'object') {
- return a === b;
- }
- if (Object.keys(a).length != Object.keys(b).length) {
- return false;
- }
- for (const k in a) {
- if (a[k] !== b[k]) {
- return false;
- }
- }
- return true;
-}
-
-
-function defineReadonlyProperty(obj, key, value) {
- const copy = deepCopy(value);
- if (typeof copy == 'object') {
- Object.freeze(copy);
- }
- Object.defineProperty(obj, key, {value: copy, configurable: true});
-}
-
-
-// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
-function getSync() {
- if ('sync' in chrome.storage) {
- return chrome.storage.sync;
- }
- const crappyStorage = {};
- return {
- get(key, callback) {
- callback(crappyStorage[key] || {});
- },
- set(source, callback) {
- for (const property in source) {
- if (source.hasOwnProperty(property)) {
- crappyStorage[property] = source[property];
- }
- }
- callback();
- }
- };
-}
-
-
function styleSectionsEqual(styleA, styleB) {
if (!styleA.sections || !styleB.sections) {
return undefined;
@@ -990,3 +421,136 @@ function compileStyleRegExps({style, compileAll}) {
}
}
}
+
+
+function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
+ // prevent double-add on echoed invalidation
+ const cached = added && cachedStyles.byId.get(added.id);
+ if (cached) {
+ return;
+ }
+ if (andNotify) {
+ chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId});
+ }
+ if (!cachedStyles.list) {
+ return;
+ }
+ if (updated) {
+ const cached = cachedStyles.byId.get(updated.id);
+ if (cached) {
+ Object.assign(cached, updated);
+ //console.debug('cache: updated', updated);
+ }
+ cachedStyles.filters.clear();
+ return;
+ }
+ if (added) {
+ cachedStyles.list.push(added);
+ cachedStyles.byId.set(added.id, added);
+ //console.debug('cache: added', added);
+ cachedStyles.filters.clear();
+ return;
+ }
+ if (deletedId != undefined) {
+ const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style;
+ if (deletedStyle) {
+ const cachedIndex = cachedStyles.list.indexOf(deletedStyle);
+ cachedStyles.list.splice(cachedIndex, 1);
+ cachedStyles.byId.delete(deletedId);
+ //console.debug('cache: deleted', deletedStyle);
+ cachedStyles.filters.clear();
+ return;
+ }
+ }
+ cachedStyles.list = null;
+ //console.debug('cache cleared');
+ cachedStyles.filters.clear();
+}
+
+
+function cleanupCachedFilters({force = false} = {}) {
+ if (!force) {
+ // sliding timer for 1 second
+ clearTimeout(cleanupCachedFilters.timeout);
+ cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true});
+ return;
+ }
+ const size = cachedStyles.filters.size;
+ const oldestHit = cachedStyles.filters.values().next().value.lastHit;
+ const now = Date.now();
+ const timeSpan = now - oldestHit;
+ const recencyWeight = 5 / size;
+ const hitWeight = 1 / 4; // we make ~4 hits per URL
+ const lastHitWeight = 10;
+ // delete the oldest 10%
+ [...cachedStyles.filters.entries()]
+ .map(([id, v], index) => ({
+ id,
+ weight:
+ index * recencyWeight +
+ v.hits * hitWeight +
+ (v.lastHit - oldestHit) / timeSpan * lastHitWeight,
+ }))
+ .sort((a, b) => a.weight - b.weight)
+ .slice(0, size / 10 + 1)
+ .forEach(({id}) => cachedStyles.filters.delete(id));
+ cleanupCachedFilters.timeout = 0;
+}
+
+
+function addMissingStyleTargets(style) {
+ style.sections = (style.sections || []).map(section =>
+ Object.assign({
+ urls: [],
+ urlPrefixes: [],
+ domains: [],
+ regexps: [],
+ }, section)
+ );
+}
+
+
+function reportError(...args) {
+ for (const arg of args) {
+ if ('message' in arg) {
+ console.log(arg.message);
+ }
+ }
+}
+
+
+function fixBoolean(b) {
+ if (typeof b != 'undefined') {
+ return b != 'false';
+ }
+ return null;
+}
+
+
+function getDomains(url) {
+ if (url.indexOf('file:') == 0) {
+ return [];
+ }
+ let d = /.*?:\/*([^/:]+)/.exec(url)[1];
+ const domains = [d];
+ while (d.indexOf('.') != -1) {
+ d = d.substring(d.indexOf('.') + 1);
+ domains.push(d);
+ }
+ return domains;
+}
+
+
+function getType(o) {
+ if (typeof o == 'undefined' || typeof o == 'string') {
+ return typeof o;
+ }
+ // with the persistent cachedStyles the Array reference is usually different
+ // so let's check for e.g. type of 'every' which is only present on arrays
+ // (in the context of our extension)
+ if (o instanceof Array || typeof o.every == 'function') {
+ return 'array';
+ }
+ console.warn('Unsupported type:', o);
+ return 'undefined';
+}
diff --git a/update.js b/update.js
index bc85d283..3ff46ebf 100644
--- a/update.js
+++ b/update.js
@@ -1,4 +1,5 @@
-/* globals getStyles */
+/* eslint brace-style: 1, arrow-parens: 1, space-before-function-paren: 1, arrow-body-style: 1 */
+/* globals getStyles, saveStyle */
'use strict';
// TODO: refactor to make usable in manage::Updater