ESLint: customize the rules; fix the issues

This commit is contained in:
tophf 2017-03-26 05:30:59 +03:00
parent 657db366c9
commit ac4a420e2b
16 changed files with 1896 additions and 1715 deletions

118
.eslintrc
View File

@ -1,52 +1,62 @@
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md # https://github.com/eslint/eslint/blob/master/docs/rules/README.md
parserOptions: parserOptions:
ecmaVersion: 2017 ecmaVersion: 2015
env: env:
browser: true browser: true
commonjs: true
es6: true es6: true
webextensions: true webextensions: true
globals: globals:
CodeMirror: false # messaging.js
runTryCatch: true OWN_ORIGIN: false
getStylesSafe: true KEEP_CHANNEL_OPEN: false
getStyles: true configureCommands: false
updateIcon: true notifyAllTabs: false
saveStyle: true refreshAllTabs: false
invalidateCache: true updateIcon: false
getDatabase: true getActiveTab: false
getActiveTabRealURL: false
getTabRealURL: false
openURL: false
activateTab: false
stringAsRegExp: false
wildcardAsRegExp: false
# localization.js
template: false
t: false
o: false
tE: false
tHTML: false
tNodeList: false
tDocLoader: false
# dom.js
onDOMready: false
getClickedStyleId: false
getClickedStyleElement: false
scrollElementIntoView: false
animateElement: false
$: false
$$: false
# storage.js
prefs: false prefs: false
reportError: true cachedStyles: false
getActiveTab: true sessionStorageHash: false
t: true getStylesSafe: false
getCodeMirrorThemes: true invalidateCache: false
setupLivePrefs: true saveStyle: false
sessionStorageHash: true enableStyle: false
template: true deleteStyle: false
tE: true fixBoolean: false
tHTML: true getDomains: false
CSSLint: true getType: false
enableStyle: true getApplicableSections: false
deleteStyle: true isCheckbox: false
getType: true runTryCatch: false
importStyles: true setupLivePrefs: false
getActiveTabRealURL: true getCodeMirrorThemes: false
openURL: true styleSectionsEqual: false
$: true
$$: true
animateElement: true
scrollElementIntoView: true
getClickedStyleElement: true
getClickedStyleId: true
onDOMready: true
getDomains: true
webSqlStorage: true
notifyAllTabs: true
handleUpdate: true
handleDelete: true
rules: rules:
accessor-pairs: [2] accessor-pairs: [2]
@ -56,7 +66,7 @@ rules:
arrow-parens: [2, as-needed] arrow-parens: [2, as-needed]
arrow-spacing: [2, {before: true, after: true}] arrow-spacing: [2, {before: true, after: true}]
block-scoped-var: [2] block-scoped-var: [2]
brace-style: [2, 1tbs, {allowSingleLine: true}] brace-style: [2, 1tbs, {allowSingleLine: false}]
camelcase: [2, {properties: never}] camelcase: [2, {properties: never}]
class-methods-use-this: [2] class-methods-use-this: [2]
comma-dangle: [0] comma-dangle: [0]
@ -77,16 +87,15 @@ rules:
func-names: [0] func-names: [0]
generator-star-spacing: [2, before] generator-star-spacing: [2, before]
global-require: [0] global-require: [0]
guard-for-in: [2] guard-for-in: [0] # not needed for our non-OOP stuff
handle-callback-err: [2, ^(err|error)$] handle-callback-err: [2, ^(err|error)$]
id-blacklist: [0] id-blacklist: [0]
id-length: [0] id-length: [0]
id-match: [0] id-match: [0]
indent: [2, 2, {VariableDeclarator: 0}] indent: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}]
jsx-quotes: [0] jsx-quotes: [0]
key-spacing: [0] key-spacing: [0]
keyword-spacing: [2] keyword-spacing: [2]
linebreak-style: [2, unix]
lines-around-comment: [0] lines-around-comment: [0]
lines-around-directive: [0] lines-around-directive: [0]
max-len: [2, {code: 120, ignoreComments: true, ignoreRegExpLiterals: true}] max-len: [2, {code: 120, ignoreComments: true, ignoreRegExpLiterals: true}]
@ -107,7 +116,7 @@ rules:
no-case-declarations: [2] no-case-declarations: [2]
no-class-assign: [2] no-class-assign: [2]
no-cond-assign: [2, except-parens] no-cond-assign: [2, except-parens]
no-confusing-arrow: [2] no-confusing-arrow: [2, {allowParens: true}]
no-const-assign: [2] no-const-assign: [2]
no-constant-condition: [0] no-constant-condition: [0]
no-continue: [0] no-continue: [0]
@ -134,11 +143,11 @@ rules:
no-extra-label: [0] no-extra-label: [0]
no-extra-parens: [0] no-extra-parens: [0]
no-extra-semi: [2] no-extra-semi: [2]
no-fallthrough: [2] no-fallthrough: [2, {commentPattern: fallthrough.*}]
no-floating-decimal: [0] no-floating-decimal: [0]
no-func-assign: [2] no-func-assign: [2]
no-global-assign: [2] no-global-assign: [2]
no-implicit-coercion: [2] no-implicit-coercion: [1]
no-implicit-globals: [0] no-implicit-globals: [0]
no-implied-eval: [2] no-implied-eval: [2]
no-inline-comments: [0] no-inline-comments: [0]
@ -148,7 +157,7 @@ rules:
no-irregular-whitespace: [2] no-irregular-whitespace: [2]
no-iterator: [2] no-iterator: [2]
no-label-var: [2] no-label-var: [2]
no-labels: [2] no-labels: [2, {allowLoop: true}]
no-lone-blocks: [2] no-lone-blocks: [2]
no-lonely-if: [0] no-lonely-if: [0]
no-loop-func: [0] no-loop-func: [0]
@ -158,7 +167,7 @@ rules:
no-mixed-spaces-and-tabs: [2] no-mixed-spaces-and-tabs: [2]
no-multi-spaces: [0] no-multi-spaces: [0]
no-multi-str: [2] no-multi-str: [2]
no-multiple-empty-lines: [2, {max: 1, maxEOF: 0, maxBOF: 0}] no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}]
no-native-reassign: [2] no-native-reassign: [2]
no-negated-condition: [0] no-negated-condition: [0]
no-negated-in-lhs: [2] no-negated-in-lhs: [2]
@ -193,7 +202,7 @@ rules:
no-tabs: [2] no-tabs: [2]
no-template-curly-in-string: [2] no-template-curly-in-string: [2]
no-this-before-super: [2] no-this-before-super: [2]
no-throw-literal: [2] no-throw-literal: [0]
no-trailing-spaces: [2] no-trailing-spaces: [2]
no-undef-init: [2] no-undef-init: [2]
no-undef: [2] no-undef: [2]
@ -207,29 +216,30 @@ rules:
no-unsafe-negation: [2] no-unsafe-negation: [2]
no-unused-expressions: [2] no-unused-expressions: [2]
no-unused-labels: [0] no-unused-labels: [0]
no-unused-vars: [2, {args: all, varsIgnorePattern: clearError, argsIgnorePattern: ^_}] no-unused-vars: [1, {args: all, vars: local, varsIgnorePattern: clearError, argsIgnorePattern: ^_}]
no-use-before-define: [2, nofunc] no-use-before-define: [2, nofunc]
no-useless-call: [2] no-useless-call: [2]
no-useless-computed-key: [2] no-useless-computed-key: [2]
no-useless-concat: [2] no-useless-concat: [2]
no-useless-constructor: [2] no-useless-constructor: [2]
no-useless-escape: [2] no-useless-escape: [2]
no-var: [0] no-var: [1]
no-warning-comments: [0] no-warning-comments: [0]
no-whitespace-before-property: [2] no-whitespace-before-property: [2]
no-with: [2] no-with: [2]
object-curly-newline: [0] object-curly-newline: [0]
object-curly-spacing: [2, never] object-curly-spacing: [2, never]
object-shorthand: [0] object-shorthand: [0]
one-var-declaration-per-line: [0] one-var-declaration-per-line: [1]
one-var: [0] one-var: [0]
operator-assignment: [2, always] operator-assignment: [2, always]
operator-linebreak: [2, after] operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}]
padded-blocks: [2, never] padded-blocks: [2, never]
prefer-numeric-literals: [2] prefer-numeric-literals: [2]
prefer-rest-params: [0] prefer-rest-params: [0]
prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}]
quote-props: [0] quote-props: [0]
quotes: [2, double, avoid-escape] quotes: [1, single, avoid-escape]
radix: [2, as-needed] radix: [2, as-needed]
require-jsdoc: [0] require-jsdoc: [0]
require-yield: [2] require-yield: [2]
@ -242,7 +252,7 @@ rules:
space-in-parens: [2, never] space-in-parens: [2, never]
space-infix-ops: [2] space-infix-ops: [2]
space-unary-ops: [2] space-unary-ops: [2]
spaced-comment: [2, always, {markers: ["!"]}] spaced-comment: [0, always, {markers: ["!"]}]
strict: [2, global] strict: [2, global]
symbol-description: [2] symbol-description: [2]
template-curly-spacing: [2, never] template-curly-spacing: [2, never]

287
apply.js
View File

@ -1,21 +1,25 @@
// using ES5 syntax because ES6 is fast only since around Chrome 55 // Not using some slow features of ES6, see http://kpdecker.github.io/six-speed/
// so we'll wait until Chrome 60 arguably before converting // like destructring, classes, defaults, spread, calculated key names
/* eslint no-var: 0 */
'use strict';
var g_disableAll = false; var disableAll = false;
var g_styleElements = {}; var styleElements = new Map();
var iframeObserver;
var retiredStyleIds = []; var retiredStyleIds = [];
var iframeObserver;
initObserver(); initObserver();
requestStyles(); requestStyles();
chrome.runtime.onMessage.addListener(applyOnMessage);
function requestStyles(options = {}) {
function requestStyles(options) {
// If this is a Stylish page (Edit Style or Manage Styles), // If this is a Stylish page (Edit Style or Manage Styles),
// we'll request the styles directly to minimize delay and flicker, // we'll request the styles directly to minimize delay and flicker,
// unless Chrome still starts up and the background page isn't fully loaded. // unless Chrome still starts up and the background page isn't fully loaded.
// (Note: in this case the function may be invoked again from applyStyles.) // (Note: in this case the function may be invoked again from applyStyles.)
var request = Object.assign({ const request = Object.assign({
method: "getStyles", method: 'getStyles',
matchUrl: location.href, matchUrl: location.href,
enabled: true, enabled: true,
asHash: true, asHash: true,
@ -27,151 +31,153 @@ function requestStyles(options = {}) {
} }
} }
chrome.runtime.onMessage.addListener(applyOnMessage);
function applyOnMessage(request, sender, sendResponse) { function applyOnMessage(request, sender, sendResponse) {
// Also handle special request just for the pop-up // Also handle special request just for the pop-up
switch (request.method == "updatePopup" ? request.reason : request.method) { switch (request.method == 'updatePopup' ? request.reason : request.method) {
case "styleDeleted":
case 'styleDeleted':
removeStyle(request.id, document); removeStyle(request.id, document);
break; break;
case "styleUpdated":
case 'styleUpdated':
if (request.codeIsUpdated === false) { if (request.codeIsUpdated === false) {
applyStyleState(request.style.id, request.style.enabled, document); applyStyleState(request.style.id, request.style.enabled, document);
break; break;
} }
if (request.style.enabled) { if (!request.style.enabled) {
retireStyle(request.style.id);
// fallthrough to "styleAdded"
} else {
removeStyle(request.style.id, document); removeStyle(request.style.id, document);
break; break;
} }
case "styleAdded": retireStyle(request.style.id);
// fallthrough to 'styleAdded'
case 'styleAdded':
if (request.style.enabled) { if (request.style.enabled) {
chrome.runtime.sendMessage({method: "getStyles", matchUrl: location.href, enabled: true, id: request.style.id, asHash: true}, applyStyles); requestStyles({id: request.style.id}, applyStyles);
} }
break; break;
case "styleApply":
case 'styleApply':
applyStyles(request.styles); applyStyles(request.styles);
break; break;
case "styleReplaceAll":
case 'styleReplaceAll':
replaceAll(request.styles, document); replaceAll(request.styles, document);
break; break;
case "styleDisableAll":
disableAll(request.disableAll); case 'styleDisableAll':
doDisableAll(request.disableAll);
break; break;
case "ping":
case 'ping':
sendResponse(true); sendResponse(true);
break; break;
} }
} }
function disableAll(disable) {
if (!disable === !g_disableAll) { function doDisableAll(disable) {
if (!disable === !disableAll) {
return; return;
} }
g_disableAll = disable; disableAll = disable;
if (g_disableAll) { if (disableAll) {
iframeObserver.disconnect(); iframeObserver.disconnect();
} }
disableSheets(g_disableAll, document); disableSheets(disableAll, document);
if (!g_disableAll && document.readyState != "loading") { if (!disableAll && document.readyState != 'loading') {
iframeObserver.start(); iframeObserver.start();
} }
function disableSheets(disable, doc) { function disableSheets(disable, doc) {
Array.prototype.forEach.call(doc.styleSheets, function(stylesheet) { Array.prototype.forEach.call(doc.styleSheets, stylesheet => {
if (stylesheet.ownerNode.classList.contains("stylus") if (stylesheet.ownerNode.classList.contains('stylus')
&& stylesheet.disabled != disable) { && stylesheet.disabled != disable) {
stylesheet.disabled = disable; stylesheet.disabled = disable;
} }
}); });
getDynamicIFrames(doc).forEach(function(iframe) { for (const iframe of getDynamicIFrames(doc)) {
if (!disable) { if (!disable) {
// update the IFRAME if it was created while the observer was disconnected // update the IFRAME if it was created while the observer was disconnected
addDocumentStylesToIFrame(iframe); addDocumentStylesToIFrame(iframe);
} }
disableSheets(disable, iframe.contentDocument); disableSheets(disable, iframe.contentDocument);
});
} }
} }
}
function applyStyleState(id, enabled, doc) { function applyStyleState(id, enabled, doc) {
var e = doc.getElementById("stylus-" + id); const el = doc.getElementById('stylus-' + id);
if (!e) { if (el) {
if (enabled) { el.sheet.disabled = !enabled;
processDynamicIFrames(doc, applyStyleState, id, enabled);
} else if (enabled) {
requestStyles({id}); requestStyles({id});
} }
} else {
e.sheet.disabled = !enabled;
getDynamicIFrames(doc).forEach(function(iframe) {
applyStyleState(id, iframe.contentDocument);
});
}
} }
function removeStyle(id, doc) { function removeStyle(id, doc) {
var e = doc.getElementById("stylus-" + id); styleElements.delete('stylus-' + id);
delete g_styleElements["stylus-" + id]; const el = doc.getElementById('stylus-' + id);
if (e) { if (el) {
e.remove(); el.remove();
} }
if (doc == document && Object.keys(g_styleElements).length == 0) { if (doc == document && !styleElements.size) {
iframeObserver.disconnect(); iframeObserver.disconnect();
} }
getDynamicIFrames(doc).forEach(function(iframe) { processDynamicIFrames(doc, removeStyle, id);
removeStyle(id, iframe.contentDocument);
});
} }
// to avoid page flicker when the style is updated // to avoid page flicker when the style is updated
// instead of removing it immediately we rename its ID and queue it // instead of removing it immediately we rename its ID and queue it
// to be deleted in applyStyles after a new version is fetched and applied // to be deleted in applyStyles after a new version is fetched and applied
function retireStyle(id, doc) { function retireStyle(id, doc) {
var deadID = "ghost-" + id; const deadID = 'ghost-' + id;
if (!doc) { if (!doc) {
doc = document; doc = document;
retiredStyleIds.push(deadID); retiredStyleIds.push(deadID);
delete g_styleElements["stylus-" + id]; styleElements.delete('stylus-' + id);
// in case something went wrong and new style was never applied // in case something went wrong and new style was never applied
setTimeout(removeStyle.bind(null, deadID, doc), 1000); setTimeout(removeStyle, 1000, deadID, doc);
} }
var e = doc.getElementById("stylus-" + id); const el = doc.getElementById('stylus-' + id);
if (e) { if (el) {
e.id = "stylus-" + deadID; el.id = 'stylus-' + deadID;
} }
getDynamicIFrames(doc).forEach(function(iframe) { processDynamicIFrames(doc, retireStyle, id);
retireStyle(id, iframe.contentDocument);
});
} }
function applyStyles(styleHash) { function applyStyles(styleHash) {
if (!styleHash) { // Chrome is starting up if (!styleHash) { // Chrome is starting up
requestStyles(); requestStyles();
return; return;
} }
if ("disableAll" in styleHash) { if ('disableAll' in styleHash) {
disableAll(styleHash.disableAll); doDisableAll(styleHash.disableAll);
delete styleHash.disableAll; delete styleHash.disableAll;
} }
for (var styleId in styleHash) { for (const styleId in styleHash) {
applySections(styleId, styleHash[styleId]); applySections(styleId, styleHash[styleId]);
} }
if (Object.keys(g_styleElements).length) { if (styleElements.size) {
// when site response is application/xml Chrome displays our style elements // when site response is application/xml Chrome displays our style elements
// under document.documentElement as plain text so we need to move them into HEAD // under document.documentElement as plain text so we need to move them into HEAD
// (which already is autogenerated at this moment for the xml response) // which is already autogenerated at this moment
if (document.head && document.head.firstChild && document.head.firstChild.id == "xml-viewer-style") { if (document.head && document.head.firstChild && document.head.firstChild.id == 'xml-viewer-style') {
for (var id in g_styleElements) { for (const id of styleElements.keys()) {
document.head.appendChild(document.getElementById(id)); document.head.appendChild(document.getElementById(id));
} }
} }
document.addEventListener("DOMContentLoaded", onDOMContentLoaded); document.addEventListener('DOMContentLoaded', onDOMContentLoaded);
} }
if (retiredStyleIds.length) { if (retiredStyleIds.length) {
@ -183,116 +189,136 @@ function applyStyles(styleHash) {
} }
} }
function onDOMContentLoaded() { function onDOMContentLoaded() {
addDocumentStylesToAllIFrames(); addDocumentStylesToAllIFrames();
iframeObserver.start(); iframeObserver.start();
} }
function applySections(styleId, sections) { function applySections(styleId, sections) {
var styleElement = document.getElementById("stylus-" + styleId); let el = document.getElementById('stylus-' + styleId);
// Already there. // Already there.
if (styleElement) { if (el) {
return; return;
} }
if (document.documentElement instanceof SVGSVGElement) { if (document.documentElement instanceof SVGSVGElement) {
// SVG document, make an SVG style element. // SVG document, make an SVG style element.
styleElement = document.createElementNS("http://www.w3.org/2000/svg", "style"); el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
} else { } else {
// This will make an HTML style element. If there's SVG embedded in an HTML document, this works on the SVG too. // This will make an HTML style element. If there's SVG embedded in an HTML document, this works on the SVG too.
styleElement = document.createElement("style"); el = document.createElement('style');
} }
styleElement.setAttribute("id", "stylus-" + styleId); el.setAttribute('id', 'stylus-' + styleId);
styleElement.setAttribute("class", "stylus"); el.setAttribute('class', 'stylus');
styleElement.setAttribute("type", "text/css"); el.setAttribute('type', 'text/css');
styleElement.appendChild(document.createTextNode(sections.map(function(section) { el.appendChild(document.createTextNode(sections.map(section => section.code).join('\n')));
return section.code; addStyleElement(el, document);
}).join("\n"))); styleElements.set(el.id, el);
addStyleElement(styleElement, document);
g_styleElements[styleElement.id] = styleElement;
} }
function addStyleElement(styleElement, doc) {
if (!doc.documentElement || doc.getElementById(styleElement.id)) { function addStyleElement(el, doc) {
if (!doc.documentElement || doc.getElementById(el.id)) {
return; return;
} }
doc.documentElement.appendChild(doc.importNode(styleElement, true)) doc.documentElement.appendChild(doc.importNode(el, true))
.disabled = g_disableAll; .disabled = disableAll;
getDynamicIFrames(doc).forEach(function(iframe) { for (const iframe of getDynamicIFrames(doc)) {
if (iframeIsLoadingSrcDoc(iframe)) { if (iframeIsLoadingSrcDoc(iframe)) {
addStyleToIFrameSrcDoc(iframe, styleElement); addStyleToIFrameSrcDoc(iframe, el);
} else { } else {
addStyleElement(styleElement, iframe.contentDocument); addStyleElement(el, iframe.contentDocument);
}
} }
});
} }
function addDocumentStylesToIFrame(iframe) { function addDocumentStylesToIFrame(iframe) {
var doc = iframe.contentDocument; const doc = iframe.contentDocument;
var srcDocIsLoading = iframeIsLoadingSrcDoc(iframe); const srcDocIsLoading = iframeIsLoadingSrcDoc(iframe);
for (var id in g_styleElements) { for (const el of styleElements.values()) {
if (srcDocIsLoading) { if (srcDocIsLoading) {
addStyleToIFrameSrcDoc(iframe, g_styleElements[id]); addStyleToIFrameSrcDoc(iframe, el);
} else { } else {
addStyleElement(g_styleElements[id], doc); addStyleElement(el, doc);
} }
} }
} }
function addDocumentStylesToAllIFrames() { function addDocumentStylesToAllIFrames() {
getDynamicIFrames(document).forEach(addDocumentStylesToIFrame); getDynamicIFrames(document).forEach(addDocumentStylesToIFrame);
} }
// Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs. // Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs.
function getDynamicIFrames(doc) { function getDynamicIFrames(doc) {
return Array.prototype.filter.call(doc.getElementsByTagName('iframe'), iframeIsDynamic); return [...doc.getElementsByTagName('iframe')].filter(iframeIsDynamic);
} }
function iframeIsDynamic(f) { function iframeIsDynamic(f) {
var href; let href;
if (f.src && f.src.startsWith('http') && new URL(f.src).origin != location.origin) {
return false;
}
try { try {
href = f.contentDocument.location.href; href = f.contentDocument.location.href;
} catch (ex) { } catch (ex) {
// Cross-origin, so it's not a dynamic iframe // Cross-origin, so it's not a dynamic iframe
return false; return false;
} }
return href == document.location.href || href.indexOf("about:") == 0; return href == document.location.href || href.startsWith('about:');
} }
function processDynamicIFrames(doc, fn, ...args) {
for (const iframe of [...doc.getElementsByTagName('iframe')]) {
if (iframeIsDynamic(iframe)) {
fn(...args, iframe.contentDocument);
}
}
}
function iframeIsLoadingSrcDoc(f) { function iframeIsLoadingSrcDoc(f) {
return f.srcdoc && f.contentDocument.all.length <= 3; return f.srcdoc && f.contentDocument.all.length <= 3;
// 3 nodes or less in total (html, head, body) == new empty iframe about to be overwritten by its 'srcdoc' // 3 nodes or less in total (html, head, body) == new empty iframe about to be overwritten by its 'srcdoc'
} }
function addStyleToIFrameSrcDoc(iframe, styleElement) {
if (g_disableAll) { function addStyleToIFrameSrcDoc(iframe, el) {
if (disableAll) {
return; return;
} }
iframe.srcdoc += styleElement.outerHTML; iframe.srcdoc += el.outerHTML;
// make sure the style is added in case srcdoc was malformed // make sure the style is added in case srcdoc was malformed
setTimeout(addStyleElement.bind(null, styleElement, iframe.contentDocument), 100); setTimeout(addStyleElement, 100, el, iframe.contentDocument);
} }
function replaceAll(newStyles, doc, pass2) {
var oldStyles = [].slice.call(doc.querySelectorAll("STYLE.stylus" + (pass2 ? "[id$='-ghost']" : ""))); function replaceAll(newStyles, doc) {
if (!pass2) { const oldStyles = [...doc.querySelectorAll('STYLE.stylus')];
oldStyles.forEach(function(style) { style.id += "-ghost"; }); oldStyles.forEach(style => (style.id += '-ghost'));
} processDynamicIFrames(doc, replaceAll, newStyles);
getDynamicIFrames(doc).forEach(function(iframe) { if (doc == document) {
replaceAll(newStyles, iframe.contentDocument, pass2); styleElements.clear();
});
if (doc == document && !pass2) {
g_styleElements = {};
applyStyles(newStyles); applyStyles(newStyles);
replaceAll(newStyles, doc, true); replaceAllpass2(newStyles, doc);
}
if (pass2) {
oldStyles.forEach(function(style) { style.remove(); });
} }
} }
function replaceAllpass2(newStyles, doc) {
const oldStyles = [...doc.querySelectorAll('STYLE.stylus[id$="-ghost"]')];
processDynamicIFrames(doc, replaceAllpass2, newStyles);
oldStyles.forEach(style => style.remove());
}
// Observe dynamic IFRAMEs being added // Observe dynamic IFRAMEs being added
function initObserver() { function initObserver() {
var orphanCheckTimer; let orphanCheckTimer;
iframeObserver = new MutationObserver(function(mutations) { iframeObserver = new MutationObserver(function(mutations) {
clearTimeout(orphanCheckTimer); clearTimeout(orphanCheckTimer);
@ -306,18 +332,21 @@ function initObserver() {
return; return;
} }
// move the check out of current execution context // move the check out of current execution context
// because some same-domain (!) iframes fail to load when their "contentDocument" is accessed (!) // because some same-domain (!) iframes fail to load when their 'contentDocument' is accessed (!)
// namely gmail's old chat iframe talkgadget.google.com // namely gmail's old chat iframe talkgadget.google.com
setTimeout(process.bind(null, mutations), 0); setTimeout(process, 0, mutations);
}); });
function process(mutations) { function process(mutations) {
// var is slightly faster and MutationObserver may run a lot
// eslint-disable-next-line no-var
for (var m = 0, ml = mutations.length; m < ml; m++) { for (var m = 0, ml = mutations.length; m < ml; m++) {
var mutation = mutations[m]; const mutation = mutations[m];
if (mutation.type === "childList") { if (mutation.type === 'childList') {
// eslint-disable-next-line no-var
for (var n = 0, nodes = mutation.addedNodes, nl = nodes.length; n < nl; n++) { for (var n = 0, nodes = mutation.addedNodes, nl = nodes.length; n < nl; n++) {
var node = nodes[n]; const node = nodes[n];
if (node.localName === "iframe" && iframeIsDynamic(node)) { if (node.localName === 'iframe' && iframeIsDynamic(node)) {
addDocumentStylesToIFrame(node); addDocumentStylesToIFrame(node);
} }
} }
@ -325,14 +354,14 @@ function initObserver() {
} }
} }
iframeObserver.start = function() { iframeObserver.start = () => {
// will be ignored by browser if already observing // subsequent calls are ignored if already started observing
iframeObserver.observe(document, {childList: true, subtree: true}); iframeObserver.observe(document, {childList: true, subtree: true});
} };
function orphanCheck() { function orphanCheck() {
orphanCheckTimer = 0; orphanCheckTimer = 0;
var port = chrome.runtime.connect(); const port = chrome.runtime.connect();
if (port) { if (port) {
port.disconnect(); port.disconnect();
return; return;
@ -344,7 +373,7 @@ function initObserver() {
iframeObserver.disconnect(); iframeObserver.disconnect();
iframeObserver = null; iframeObserver = null;
// we can detach event listeners // we can detach event listeners
document.removeEventListener("DOMContentLoaded", onDOMContentLoaded); document.removeEventListener('DOMContentLoaded', onDOMContentLoaded);
// we can't detach chrome.runtime.onMessage because it's no longer connected internally // we can't detach chrome.runtime.onMessage because it's no longer connected internally
// we can destroy global functions in this context to free up memory // we can destroy global functions in this context to free up memory
@ -356,18 +385,20 @@ function initObserver() {
'applyOnMessage', 'applyOnMessage',
'applySections', 'applySections',
'applyStyles', 'applyStyles',
'disableAll', 'doDisableAll',
'getDynamicIFrames', 'getDynamicIFrames',
'processDynamicIFrames',
'iframeIsDynamic', 'iframeIsDynamic',
'iframeIsLoadingSrcDoc', 'iframeIsLoadingSrcDoc',
'initObserver', 'initObserver',
'removeStyle', 'removeStyle',
'replaceAll', 'replaceAll',
'replaceAllpass2',
'requestStyles', 'requestStyles',
'retireStyle' 'retireStyle'
].forEach(fn => window[fn] = null); ].forEach(fn => (window[fn] = null));
// we can destroy global variables // we can destroy global variables
g_styleElements = iframeObserver = retiredStyleIds = null; styleElements = iframeObserver = retiredStyleIds = null;
} }
} }

View File

@ -1,17 +1,28 @@
/* globals openURL, wildcardAsRegExp, KEEP_CHANNEL_OPEN */ /* global getDatabase, getStyles, reportError */
'use strict';
// This happens right away, sometimes so fast that the content script isn't even ready. That's // This happens right away, sometimes so fast that the content script isn't even ready. That's
// why the content script also asks for this stuff. // why the content script also asks for this stuff.
chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, 'styleApply')); chrome.webNavigation.onCommitted.addListener(data => {
chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, 'styleReplaceAll')); webNavigationListener('styleApply', data);
chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null)); });
chrome.webNavigation.onHistoryStateUpdated.addListener(data => {
webNavigationListener('styleReplaceAll', data);
});
chrome.webNavigation.onBeforeNavigate.addListener(data => {
webNavigationListener(null, data);
});
function webNavigationListener(method, data) { function webNavigationListener(method, data) {
getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => { getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => {
// we can't inject chrome:// and chrome-extension:// pages except our own // we can't inject chrome:// and chrome-extension:// pages except our own
// that request the styles on their own, so we'll only update the icon // that request the styles on their own, so we'll only update the icon
if (method && !data.url.startsWith('chrome')) { if (method && !data.url.startsWith('chrome')) {
chrome.tabs.sendMessage(data.tabId, {method, styles}, {frameId: data.frameId}); chrome.tabs.sendMessage(
data.tabId,
{method, styles},
{frameId: data.frameId});
} }
// main page frame id is 0 // main page frame id is 0
if (data.frameId == 0) { if (data.frameId == 0) {
@ -21,23 +32,29 @@ function webNavigationListener(method, data) {
} }
// catch direct URL hash modifications not invoked via HTML5 history API // catch direct URL hash modifications not invoked via HTML5 history API
const tabUrlHasHash = new Set();
var tabUrlHasHash = {}; chrome.tabs.onUpdated.addListener((tabId, info, tab) => {
chrome.tabs.onUpdated.addListener(function(tabId, info, tab) { if (info.status != 'loading' || !info.url) {
if (info.status == "loading" && info.url) { return;
if (info.url.indexOf('#') > 0) { }
tabUrlHasHash[tabId] = true; if (info.url.includes('#')) {
} else if (tabUrlHasHash[tabId]) { tabUrlHasHash.add(tabId);
delete tabUrlHasHash[tabId]; } else if (tabUrlHasHash.has(tabId)) {
tabUrlHasHash.delete(tabId);
} else { } else {
// do nothing since the tab neither had # before nor has # now // do nothing since the tab neither had # before nor has # now
return; return;
} }
webNavigationListener("styleReplaceAll", {tabId: tabId, frameId: 0, url: info.url}); webNavigationListener('styleReplaceAll', {
} tabId: tabId,
frameId: 0,
url: info.url,
}); });
chrome.tabs.onRemoved.addListener(function(tabId, info) { });
delete tabUrlHasHash[tabId];
chrome.tabs.onRemoved.addListener(tabId => {
tabUrlHasHash.delete(tabId);
}); });
// messaging // messaging
@ -46,8 +63,9 @@ chrome.runtime.onMessage.addListener(onBackgroundMessage);
function onBackgroundMessage(request, sender, sendResponse) { function onBackgroundMessage(request, sender, sendResponse) {
switch (request.method) { switch (request.method) {
case "getStyles":
var styles = getStyles(request, sendResponse); case 'getStyles':
var styles = getStyles(request, sendResponse); // eslint-disable-line no-var
// check if this is a main content frame style enumeration // check if this is a main content frame style enumeration
if (request.matchUrl && !request.id if (request.matchUrl && !request.id
&& sender && sender.tab && sender.frameId == 0 && sender && sender.tab && sender.frameId == 0
@ -55,31 +73,33 @@ function onBackgroundMessage(request, sender, sendResponse) {
updateIcon(sender.tab, styles); updateIcon(sender.tab, styles);
} }
return KEEP_CHANNEL_OPEN; return KEEP_CHANNEL_OPEN;
case "saveStyle":
case 'saveStyle':
saveStyle(request).then(sendResponse); saveStyle(request).then(sendResponse);
return KEEP_CHANNEL_OPEN; return KEEP_CHANNEL_OPEN;
case "invalidateCache":
if (typeof invalidateCache != "undefined") { case 'invalidateCache':
if (typeof invalidateCache != 'undefined') {
invalidateCache(false, request); invalidateCache(false, request);
} }
break; break;
case "healthCheck":
getDatabase(function() { sendResponse(true); }, function() { sendResponse(false); }); case 'healthCheck':
getDatabase(
() => sendResponse(true),
() => sendResponse(false));
return KEEP_CHANNEL_OPEN; return KEEP_CHANNEL_OPEN;
case "openURL":
openURL(request); case 'styleDisableAll':
break;
case "styleDisableAll":
// fallthru to prefChanged
request = {prefName: 'disableAll', value: request.disableAll}; request = {prefName: 'disableAll', value: request.disableAll};
case "prefChanged": // fallthrough to prefChanged
case 'prefChanged':
// eslint-disable-next-line no-use-before-define
if (typeof request.value == 'boolean' && contextMenus[request.prefName]) { if (typeof request.value == 'boolean' && contextMenus[request.prefName]) {
chrome.contextMenus.update(request.prefName, {checked: request.value}); chrome.contextMenus.update(request.prefName, {checked: request.value});
} }
break; break;
case "refreshAllTabs":
refreshAllTabs().then(sendResponse);
return KEEP_CHANNEL_OPEN;
} }
} }
@ -159,22 +179,20 @@ getDatabase(function() {}, reportError);
// When an edit page gets attached or detached, remember its state // When an edit page gets attached or detached, remember its state
// so we can do the same to the next one to open. // so we can do the same to the next one to open.
var editFullUrl = chrome.extension.getURL("edit.html"); const editFullUrl = OWN_ORIGIN + 'edit.html';
chrome.tabs.onAttached.addListener(function(tabId, data) { chrome.tabs.onAttached.addListener((tabId, data) => {
chrome.tabs.get(tabId, function(tabData) { chrome.tabs.get(tabId, tabData => {
if (tabData.url.indexOf(editFullUrl) == 0) { if (tabData.url.startsWith(editFullUrl)) {
chrome.windows.get(tabData.windowId, {populate: true}, function(win) { chrome.windows.get(tabData.windowId, {populate: true}, win => {
// If there's only one tab in this window, it's been dragged to new window // If there's only one tab in this window, it's been dragged to new window
prefs.set("openEditInWindow", win.tabs.length == 1); prefs.set('openEditInWindow', win.tabs.length == 1);
}); });
} }
}); });
}); });
var codeMirrorThemes; var codeMirrorThemes; // eslint-disable-line no-var
getCodeMirrorThemes(function(themes) { getCodeMirrorThemes(themes => (codeMirrorThemes = themes));
codeMirrorThemes = themes;
});
// do not use prefs.get('version', null) as it might not yet be available // do not use prefs.get('version', null) as it might not yet be available
chrome.storage.local.get('version', prefs => { chrome.storage.local.get('version', prefs => {
@ -183,31 +201,33 @@ chrome.storage.local.get('version', prefs => {
if (!prefs.version) { if (!prefs.version) {
// do not display the FAQs page in development mode // do not display the FAQs page in development mode
if ('update_url' in chrome.runtime.getManifest()) { if ('update_url' in chrome.runtime.getManifest()) {
let version = chrome.runtime.getManifest().version; const version = chrome.runtime.getManifest().version;
chrome.storage.local.set({ chrome.storage.local.set({version}, () => {
version
}, () => {
window.setTimeout(() => { window.setTimeout(() => {
chrome.tabs.create({ chrome.tabs.create({
url: 'http://add0n.com/stylus.html?version=' + version + '&type=install' url: `http://add0n.com/stylus.html?version=${version}&type=install`
}); });
}, 3000); }, 3000);
}) });
} }
} }
}); });
injectContentScripts(); injectContentScripts();
function injectContentScripts() { function injectContentScripts() {
const contentScripts = chrome.app.getDetails().content_scripts; const contentScripts = chrome.app.getDetails().content_scripts;
for (let cs of contentScripts) { for (const cs of contentScripts) {
cs.matches = cs.matches.map(m => m == '<all_urls>' ? m : wildcardAsRegExp(m)); cs.matches = cs.matches.map(m => (
m == '<all_urls>' ? m : wildcardAsRegExp(m)
));
} }
// also inject in chrome://newtab/ page
chrome.tabs.query({url: '*://*/*'}, tabs => { chrome.tabs.query({url: '*://*/*'}, tabs => {
for (let tab of tabs) { for (const tab of tabs) {
for (let cs of contentScripts) { for (const cs of contentScripts) {
for (let m of cs.matches) { for (const m of cs.matches) {
if (m == '<all_urls>' || tab.url.match(m)) { if (m == '<all_urls>' || tab.url.match(m)) {
chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => { chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => {
if (!pong) { if (!pong) {
@ -229,5 +249,5 @@ function injectContentScripts() {
function ignoreChromeError() { function ignoreChromeError() {
chrome.runtime.lastError; chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
} }

View File

@ -1,4 +1,3 @@
/* globals getStyles, saveStyle, invalidateCache, refreshAllTabs */
'use strict'; 'use strict';
const STYLISH_DUMP_FILE_EXT = '.txt'; const STYLISH_DUMP_FILE_EXT = '.txt';
@ -228,7 +227,7 @@ function importFromString(jsonString) {
$('#file-all-styles').onclick = () => { $('#file-all-styles').onclick = () => {
getStyles({}, function (styles) { getStylesSafe().then(styles => {
const text = JSON.stringify(styles, null, '\t'); const text = JSON.stringify(styles, null, '\t');
const fileName = generateFileName(); const fileName = generateFileName();

View File

@ -1,4 +1,5 @@
/* globals stringAsRegExp */ /* eslint no-tabs: 0, no-var: 0, indent: [2, tab, {VariableDeclarator: 0, SwitchCase: 1}], quotes: 0 */
/* global CodeMirror */
"use strict"; "use strict";
var styleId = null; var styleId = null;
@ -29,7 +30,7 @@ Array.prototype.rotate = function(amount) { // negative amount == rotate left
var r = this.slice(-amount, this.length); var r = this.slice(-amount, this.length);
Array.prototype.push.apply(r, this.slice(0, this.length - r.length)); Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
return r; return r;
} };
Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }}); Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }});

View File

@ -1,11 +1,14 @@
'use strict';
setTimeout(healthCheck, 0); setTimeout(healthCheck, 0);
function healthCheck() { function healthCheck() {
chrome.runtime.sendMessage({method: "healthCheck"}, function(ok) { chrome.runtime.sendMessage({method: 'healthCheck'}, ok => {
if (ok === undefined) { // Chrome is starting up if (ok === undefined) {
// Chrome is starting up
healthCheck(); healthCheck();
} else if (!ok && confirm(t("dbError"))) { } else if (!ok && confirm(t('dbError'))) {
window.open("http://userstyles.org/dberror"); window.open('http://userstyles.org/dberror');
} }
}); });
} }

View File

@ -1,3 +1,6 @@
/* eslint-disable no-tabs, indent, quotes, no-var */
'use strict';
chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) { chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) {
if (response.length == 0) { if (response.length == 0) {
sendEvent("styleCanBeInstalledChrome"); sendEvent("styleCanBeInstalledChrome");
@ -17,7 +20,7 @@ chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url")
} else { } else {
getResource(getMeta("stylish-code-chrome"), function(code) { getResource(getMeta("stylish-code-chrome"), function(code) {
// this would indicate a failure (a style with settings?). // this would indicate a failure (a style with settings?).
if (code == null) { if (code === null) {
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl}); sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
} }
var json = JSON.parse(code); var json = JSON.parse(code);
@ -30,7 +33,7 @@ chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url")
// everything's the same // everything's the same
sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl}); sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl});
return; return;
}; }
} }
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl}); sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
}); });
@ -49,10 +52,12 @@ function sectionsAreEqual(a, b) {
function arraysAreEqual(a, b) { function arraysAreEqual(a, b) {
// treat empty array and undefined as equivalent // treat empty array and undefined as equivalent
if (typeof a == "undefined") if (typeof a == "undefined") {
return (typeof b == "undefined") || (b.length == 0); return (typeof b == "undefined") || (b.length == 0);
if (typeof b == "undefined") }
if (typeof b == "undefined") {
return (typeof a == "undefined") || (a.length == 0); return (typeof a == "undefined") || (a.length == 0);
}
if (a.length != b.length) { if (a.length != b.length) {
return false; return false;
} }
@ -78,7 +83,7 @@ function stylishInstallChrome() {
// check for old style json // check for old style json
var json = JSON.parse(code); var json = JSON.parse(code);
json.method = "saveStyle"; json.method = "saveStyle";
chrome.runtime.sendMessage(json, function(response) { chrome.runtime.sendMessage(json, function() {
sendEvent("styleInstalledChrome"); sendEvent("styleInstalledChrome");
}); });
}); });
@ -90,7 +95,10 @@ function stylishInstallChrome() {
document.addEventListener("stylishInstallChrome", stylishInstallChrome); document.addEventListener("stylishInstallChrome", stylishInstallChrome);
function stylishUpdateChrome() { function stylishUpdateChrome() {
orphanCheck(); orphanCheck();
chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) { chrome.runtime.sendMessage({
method: "getStyles",
url: getMeta("stylish-id-url") || location.href,
}, function(response) {
var style = response[0]; var style = response[0];
if (confirm(chrome.i18n.getMessage('styleUpdate', [style.name]))) { if (confirm(chrome.i18n.getMessage('styleUpdate', [style.name]))) {
getResource(getMeta("stylish-code-chrome"), function(code) { getResource(getMeta("stylish-code-chrome"), function(code) {
@ -126,7 +134,7 @@ function getResource(url, callback) {
callback(xhr.responseText); callback(xhr.responseText);
} }
} }
} };
if (url.length > 2000) { if (url.length > 2000) {
var parts = url.split("?"); var parts = url.split("?");
xhr.open("POST", parts[0], true); xhr.open("POST", parts[0], true);
@ -176,5 +184,5 @@ function orphanCheck() {
'sendEvent', 'sendEvent',
'stylishUpdateChrome', 'stylishUpdateChrome',
'stylishInstallChrome' 'stylishInstallChrome'
].forEach(fn => window[fn] = null); ].forEach(fn => (window[fn] = null));
} }

View File

@ -1,60 +1,63 @@
var template = {}; 'use strict';
const template = {};
tDocLoader(); tDocLoader();
function t(key, params) { function t(key, params) {
var s = chrome.i18n.getMessage(key, params) const s = chrome.i18n.getMessage(key, params);
if (s == "") { if (s == '') {
throw "Missing string '" + key + "'."; throw `Missing string "${key}"`;
} }
return s; return s;
} }
function o(key) {
document.write(t(key));
}
function tE(id, key, attr, esc) { function tE(id, key, attr, esc) {
if (attr) { if (attr) {
document.getElementById(id).setAttribute(attr, t(key)); document.getElementById(id).setAttribute(attr, t(key));
} else if (typeof esc == "undefined" || esc) { } else if (typeof esc == 'undefined' || esc) {
document.getElementById(id).appendChild(document.createTextNode(t(key))); document.getElementById(id).appendChild(document.createTextNode(t(key)));
} else { } else {
document.getElementById(id).innerHTML = t(key); document.getElementById(id).innerHTML = t(key);
} }
} }
function tHTML(html) { function tHTML(html) {
var node = document.createElement("div"); const node = document.createElement('div');
node.innerHTML = html.replace(/>\s+</g, '><'); // spaces are removed; use &nbsp; for an explicit space node.innerHTML = html.replace(/>\s+</g, '><'); // spaces are removed; use &nbsp; for an explicit space
tNodeList(node.querySelectorAll("*")); if (html.includes('i18n-')) {
var child = node.removeChild(node.firstElementChild); tNodeList(node.querySelectorAll('*'));
node.remove(); }
return child; return node.firstElementChild;
} }
function tNodeList(nodes) { function tNodeList(nodes) {
for (var n = 0; n < nodes.length; n++) { for (const node of [...nodes]) {
var node = nodes[n]; // skip non-ELEMENT_NODE
if (node.nodeType != 1) { // not an ELEMENT_NODE if (node.nodeType != 1) {
continue; continue;
} }
if (node.localName == "template") { if (node.localName == 'template') {
tNodeList(node.content.querySelectorAll("*")); tNodeList(node.content.querySelectorAll('*'));
template[node.dataset.id] = node.content.firstElementChild; template[node.dataset.id] = node.content.firstElementChild;
continue; continue;
} }
for (var a = node.attributes.length - 1; a >= 0; a--) { for (const attr of [...node.attributes]) {
var attr = node.attributes[a]; let name = attr.nodeName;
var name = attr.nodeName; if (name.indexOf('i18n-') != 0) {
if (name.indexOf("i18n-") != 0) {
continue; continue;
} }
name = name.substr(5); // "i18n-".length name = name.substr(5); // 'i18n-'.length
var value = t(attr.value); const value = t(attr.value);
switch (name) { switch (name) {
case "text": case 'text':
node.insertBefore(document.createTextNode(value), node.firstChild); node.insertBefore(document.createTextNode(value), node.firstChild);
break; break;
case "html": case 'html':
node.insertAdjacentHTML("afterbegin", value); node.insertAdjacentHTML('afterbegin', value);
break; break;
default: default:
node.setAttribute(name, value); node.setAttribute(name, value);
@ -64,17 +67,17 @@ function tNodeList(nodes) {
} }
} }
function tDocLoader() { function tDocLoader() {
// localize HEAD // localize HEAD
tNodeList(document.all); tNodeList(document.all);
// localize BODY // localize BODY
const observer = new MutationObserver(function(mutations) { const observer = new MutationObserver(mutations => {
for (var m = 0; m < mutations.length; m++) { for (const mutation of mutations) {
tNodeList(mutations[m].addedNodes); tNodeList(mutation.addedNodes);
} }
}); });
const onLoad = () => { const onLoad = () => {
tDocLoader.stop(); tDocLoader.stop();
tNodeList(document.all); tNodeList(document.all);

View File

@ -125,7 +125,6 @@
<script src="manage.js"></script> <script src="manage.js"></script>
<script src="backup/fileSaveLoad.js"></script> <script src="backup/fileSaveLoad.js"></script>
<script src="msgbox/msgbox.js"></script> <script src="msgbox/msgbox.js"></script>
<script src="msgbox/confirm.js"></script>
</body> </body>
</html> </html>

View File

@ -1,4 +1,5 @@
/* globals styleSectionsEqual */ /* global messageBox */
'use strict';
const installed = $('#installed'); const installed = $('#installed');
const TARGET_LABEL = t('appliesDisplay', '').trim(); const TARGET_LABEL = t('appliesDisplay', '').trim();
@ -11,7 +12,7 @@ getStylesSafe({code: false})
.then(initGlobalEvents); .then(initGlobalEvents);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chrome.runtime.onMessage.addListener(msg => {
switch (msg.method) { switch (msg.method) {
case 'styleUpdated': case 'styleUpdated':
case 'styleAdded': case 'styleAdded':
@ -45,7 +46,7 @@ function initGlobalEvents() {
}; };
// remember scroll position on normal history navigation // remember scroll position on normal history navigation
document.addEventListener('visibilitychange', event => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState != 'visible') { if (document.visibilityState != 'visible') {
rememberScrollPosition(); rememberScrollPosition();
} }
@ -72,7 +73,7 @@ function initGlobalEvents() {
function showStyles(styles = []) { function showStyles(styles = []) {
const sorted = styles const sorted = styles
.map(style => ({name: style.name.toLocaleLowerCase(), style})) .map(style => ({name: style.name.toLocaleLowerCase(), style}))
.sort((a, b) => a.name < b.name ? -1 : a.name == b.name ? 0 : 1); .sort((a, b) => (a.name < b.name ? -1 : a.name == b.name ? 0 : 1));
const shouldRenderAll = history.state && history.state.scrollY > innerHeight; const shouldRenderAll = history.state && history.state.scrollY > innerHeight;
const renderBin = document.createDocumentFragment(); const renderBin = document.createDocumentFragment();
tDocLoader.stop(); tDocLoader.stop();
@ -80,24 +81,28 @@ function showStyles(styles = []) {
// TODO: remember how many styles fit one page to display just that portion first next time // TODO: remember how many styles fit one page to display just that portion first next time
function renderStyles(index) { function renderStyles(index) {
const t0 = performance.now(); const t0 = performance.now();
while (index < sorted.length && (shouldRenderAll || performance.now() - t0 < 10)) { while (index < sorted.length) {
renderBin.appendChild(createStyleElement(sorted[index++].style)); renderBin.appendChild(createStyleElement(sorted[index++].style));
if (!shouldRenderAll && performance.now() - t0 > 10) {
break;
}
} }
if ($('#search').value) { if ($('#search').value) {
// re-apply filtering on history Back // re-apply filtering on history Back
searchStyles(true, renderBin); searchStyles({immediately: true, container: renderBin});
} }
installed.appendChild(renderBin); installed.appendChild(renderBin);
if (index < sorted.length) { if (index < sorted.length) {
setTimeout(renderStyles, 0, index); setTimeout(renderStyles, 0, index);
} } else if (shouldRenderAll && history.state && 'scrollY' in history.state) {
else if (shouldRenderAll && history.state && 'scrollY' in history.state) {
setTimeout(() => scrollTo(0, history.state.scrollY)); setTimeout(() => scrollTo(0, history.state.scrollY));
} }
} }
} }
// silence the inapplicable warning for async code
/* eslint no-use-before-define: [2, {"functions": false, "classes": false}] */
function createStyleElement(style) { function createStyleElement(style) {
const entry = template.style.cloneNode(true); const entry = template.style.cloneNode(true);
entry.classList.add(style.enabled ? 'enabled' : 'disabled'); entry.classList.add(style.enabled ? 'enabled' : 'disabled');
@ -131,9 +136,9 @@ function createStyleElement(style) {
regexpsBefore: '/', regexpsBefore: '/',
regexpsAfter: '/', regexpsAfter: '/',
}; };
for (let [name, target] of targets.entries()) { for (const [name, target] of targets.entries()) {
for (let section of style.sections) { for (const section of style.sections) {
for (let targetValue of section[name] || []) { for (const targetValue of section[name] || []) {
target.add( target.add(
(decorations[name + 'Before'] || '') + (decorations[name + 'Before'] || '') +
targetValue.trim() + targetValue.trim() +
@ -151,7 +156,7 @@ function createStyleElement(style) {
} else { } else {
let index = 0; let index = 0;
let container = appliesTo; let container = appliesTo;
for (let target of targetsList) { for (const target of targetsList) {
if (index > 0) { if (index > 0) {
container.appendChild(template.appliesToSeparator.cloneNode(true)); container.appendChild(template.appliesToSeparator.cloneNode(true));
} }
@ -184,8 +189,10 @@ class EntryOnClick {
} }
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const left = event.button == 0, middle = event.button == 1, const left = event.button == 0;
shift = event.shiftKey, ctrl = event.ctrlKey; const middle = event.button == 1;
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const openWindow = left && shift && !ctrl; const openWindow = left && shift && !ctrl;
const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift); const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
const openForegroundTab = (middle && shift) || (left && ctrl && shift); const openForegroundTab = (middle && shift) || (left && ctrl && shift);
@ -215,10 +222,10 @@ class EntryOnClick {
} }
static update(event) { static update(event) {
const updatedCode = getClickedStyleElement(event).updatedCode; const styleElement = getClickedStyleElement(event);
// update everything but name // update everything but name
saveStyle(Object.assign(updatedCode, { saveStyle(Object.assign(styleElement.updatedCode, {
id: element.styleId, id: styleElement.styleId,
name: null, name: null,
reason: 'update', reason: 'update',
})); }));
@ -340,10 +347,10 @@ class Updater {
} }
checkMd5() { checkMd5() {
return this.download(this.md5Url).then( return Updater.download(this.md5Url).then(
md5 => md5.length == 32 md5 => (md5.length == 32
? this.decideOnMd5(md5 != this.md5) ? this.decideOnMd5(md5 != this.md5)
: this.onFailure(-1), : this.onFailure(-1)),
this.onFailure); this.onFailure);
} }
@ -355,7 +362,7 @@ class Updater {
} }
checkFullCode({forceUpdate = false} = {}) { checkFullCode({forceUpdate = false} = {}) {
return this.download(this.url).then( return Updater.download(this.url).then(
text => this.handleJson(forceUpdate, JSON.parse(text)), text => this.handleJson(forceUpdate, JSON.parse(text)),
this.onFailure); this.onFailure);
} }
@ -391,12 +398,12 @@ class Updater {
} }
} }
download(url) { static download(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.onloadend = () => xhr.status == 200 xhr.onloadend = () => (xhr.status == 200
? resolve(xhr.responseText) ? resolve(xhr.responseText)
: reject(xhr.status); : reject(xhr.status));
if (url.length > 2000) { if (url.length > 2000) {
const [mainUrl, query] = url.split('?'); const [mainUrl, query] = url.split('?');
xhr.open('POST', mainUrl, true); xhr.open('POST', mainUrl, true);
@ -412,19 +419,19 @@ class Updater {
} }
function searchStyles(immediately, bin) { function searchStyles({immediately, container}) {
const query = $('#search').value.toLocaleLowerCase(); const query = $('#search').value.toLocaleLowerCase();
if (query == (searchStyles.lastQuery || '') && !bin) { if (query == (searchStyles.lastQuery || '') && !container) {
return; return;
} }
searchStyles.lastQuery = query; searchStyles.lastQuery = query;
if (!immediately) { if (!immediately) {
clearTimeout(searchStyles.timeout); clearTimeout(searchStyles.timeout);
searchStyles.timeout = setTimeout(doSearch, 200, true); searchStyles.timeout = setTimeout(searchStyles, 200, {immediately: true});
return; return;
} }
for (let element of (bin || installed).children) { for (const element of (container || installed).children) {
const {style} = cachedStyles.byId.get(element.styleId) || {}; const {style} = cachedStyles.byId.get(element.styleId) || {};
if (style) { if (style) {
const isMatching = !query || isMatchingText(style.name) || isMatchingStyle(style); const isMatching = !query || isMatchingText(style.name) || isMatchingStyle(style);
@ -433,8 +440,8 @@ function searchStyles(immediately, bin) {
} }
function isMatchingStyle(style) { function isMatchingStyle(style) {
for (let section of style.sections) { for (const section of style.sections) {
for (let prop in section) { for (const prop in section) {
const value = section[prop]; const value = section[prop];
switch (typeof value) { switch (typeof value) {
case 'string': case 'string':
@ -443,7 +450,7 @@ function searchStyles(immediately, bin) {
} }
break; break;
case 'object': case 'object':
for (let str of value) { for (const str of value) {
if (isMatchingText(str)) { if (isMatchingText(str)) {
return true; return true;
} }

View File

@ -1,3 +1,6 @@
/* global getStyleWithNoCode, applyOnMessage, onBackgroundMessage, getStyles */
'use strict';
// keep message channel open for sendResponse in chrome.runtime.onMessage listener // keep message channel open for sendResponse in chrome.runtime.onMessage listener
const KEEP_CHANNEL_OPEN = true; const KEEP_CHANNEL_OPEN = true;
const OWN_ORIGIN = chrome.runtime.getURL(''); const OWN_ORIGIN = chrome.runtime.getURL('');
@ -11,7 +14,7 @@ function notifyAllTabs(request) {
}); });
} }
chrome.tabs.query({}, tabs => { chrome.tabs.query({}, tabs => {
for (let tab of tabs) { for (const tab of tabs) {
chrome.tabs.sendMessage(tab.id, request); chrome.tabs.sendMessage(tab.id, request);
updateIcon(tab); updateIcon(tab);
} }
@ -35,7 +38,7 @@ function refreshAllTabs() {
// list all tabs including chrome-extension:// which can be ours // list all tabs including chrome-extension:// which can be ours
chrome.tabs.query({}, tabs => { chrome.tabs.query({}, tabs => {
const lastTab = tabs[tabs.length - 1]; const lastTab = tabs[tabs.length - 1];
for (let tab of tabs) { for (const tab of tabs) {
getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => { getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => {
const message = {method: 'styleReplaceAll', styles}; const message = {method: 'styleReplaceAll', styles};
if (tab.url == location.href && typeof applyOnMessage !== 'undefined') { if (tab.url == location.href && typeof applyOnMessage !== 'undefined') {
@ -85,7 +88,7 @@ function updateIcon(tab, styles) {
if (numStyles === undefined) { if (numStyles === undefined) {
// for 'styles' asHash:true fake the length by counting numeric ids manually // for 'styles' asHash:true fake the length by counting numeric ids manually
numStyles = 0; numStyles = 0;
for (let id of Object.keys(styles)) { for (const id of Object.keys(styles)) {
numStyles += id.match(/^\d+$/) ? 1 : 0; numStyles += id.match(/^\d+$/) ? 1 : 0;
} }
} }
@ -151,11 +154,11 @@ function openURL({url, currentWindow = true}) {
if (tabs.length) { if (tabs.length) {
activateTab(tabs[0]).then(resolve); activateTab(tabs[0]).then(resolve);
} else { } else {
getActiveTab().then(tab => getActiveTab().then(tab => (
tab && tab.url == 'chrome://newtab/' tab && tab.url == 'chrome://newtab/'
? chrome.tabs.update({url}, resolve) ? chrome.tabs.update({url}, resolve)
: chrome.tabs.create({url}, resolve) : chrome.tabs.create({url}, resolve)
); ));
} }
}); });
}); });
@ -175,13 +178,13 @@ function activateTab(tab) {
function stringAsRegExp(s, flags) { function stringAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, '\\$&'), flags); return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
} }
// expands * as .*? // expands * as .*?
function wildcardAsRegExp(s, flags) { function wildcardAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags); return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags);
} }
@ -190,9 +193,9 @@ function wildcardAsRegExp(s, flags) {
// * Functions that contain object literals that contain __proto__, or get or set declarations. // * Functions that contain object literals that contain __proto__, or get or set declarations.
const configureCommands = (() => ({ const configureCommands = (() => ({
get url() { get url() {
return navigator.userAgent.indexOf('OPR') > -1 ? return navigator.userAgent.includes('OPR')
'opera://settings/configureCommands' : ? 'opera://settings/configureCommands'
'chrome://extensions/configureCommands' : 'chrome://extensions/configureCommands';
}, },
open: () => { open: () => {
chrome.tabs.create({ chrome.tabs.create({

View File

@ -1,4 +1,3 @@
/* globals configureCommands */
'use strict'; 'use strict';
@ -20,21 +19,21 @@ function save () {
localStorage.setItem('popupWidth', enforceValueRange('popupWidth')); localStorage.setItem('popupWidth', enforceValueRange('popupWidth'));
bg.prefs.set( bg.prefs.set(
'updateInterval', 'updateInterval',
Math.max(0, +$('#updateInterval').value) Math.max(0, Number($('#updateInterval').value))
); );
// display notification // display notification
let status = $('#status'); const status = $('#status');
status.textContent = 'Options saved.'; status.textContent = 'Options saved.';
setTimeout(() => status.textContent = '', 750); setTimeout(() => (status.textContent = ''), 750);
}); });
} }
function enforceValueRange(id) { function enforceValueRange(id) {
let element = document.getElementById(id); const element = document.getElementById(id);
let value = Number(element.value);
const min = Number(element.min); const min = Number(element.min);
const max = Number(element.max); const max = Number(element.max);
let value = Number(element.value);
if (value < min || value > max) { if (value < min || value > max) {
value = Math.max(min, Math.min(max, value)); value = Math.max(min, Math.min(max, value));
element.value = value; element.value = value;
@ -53,12 +52,14 @@ $('[data-cmd="open-keyboard"]').textContent =
// actions // actions
document.onclick = e => { document.onclick = e => {
let cmd = e.target.dataset.cmd; const cmd = e.target.dataset.cmd;
let total = 0, updated = 0; let total = 0;
let updated = 0;
function update() { function update() {
$('#update-counter').textContent = `${updated}/${total}`; $('#update-counter').textContent = `${updated}/${total}`;
} }
function done(target) { function done(target) {
target.disabled = false; target.disabled = false;
window.setTimeout(() => { window.setTimeout(() => {
@ -66,27 +67,23 @@ document.onclick = e => {
}, 750); }, 750);
} }
switch (cmd) { function check() {
case 'open-manage':
openURL({url: '/manage.html'});
break;
case'check-updates':
e.target.disabled = true;
chrome.runtime.getBackgroundPage(bg => { chrome.runtime.getBackgroundPage(bg => {
bg.update.perform((cmd, value) => { bg.update.perform((cmd, value) => {
if (cmd === 'count') { switch (cmd) {
case 'count':
total = value; total = value;
if (!total) { if (!total) {
done(e.target); done(e.target);
} }
} break;
else if (cmd === 'single-updated' || cmd === 'single-skipped') { case 'single-updated':
updated += 1; case 'single-skipped':
updated++;
if (total && updated === total) { if (total && updated === total) {
done(e.target); done(e.target);
} }
break;
} }
update(); update();
}); });
@ -95,6 +92,16 @@ document.onclick = e => {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
method: 'resetInterval' method: 'resetInterval'
}); });
}
switch (cmd) {
case 'open-manage':
openURL({url: '/manage.html'});
break;
case 'check-updates':
e.target.disabled = true;
check();
break; break;
case 'open-keyboard': case 'open-keyboard':

View File

@ -1,11 +1,9 @@
/* globals configureCommands, openURL */ 'use strict';
const RX_SUPPORTED_URLS = new RegExp(
`^(file|https?|ftps?):|^${OWN_ORIGIN}`);
let installed; let installed;
getActiveTabRealURL().then(url => { getActiveTabRealURL().then(url => {
const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${OWN_ORIGIN}`);
const isUrlSupported = RX_SUPPORTED_URLS.test(url); const isUrlSupported = RX_SUPPORTED_URLS.test(url);
Promise.all([ Promise.all([
isUrlSupported ? getStylesSafe({matchUrl: url}) : null, isUrlSupported ? getStylesSafe({matchUrl: url}) : null,
@ -15,7 +13,7 @@ getActiveTabRealURL().then(url => {
}); });
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chrome.runtime.onMessage.addListener(msg => {
if (msg.method == 'updatePopup') { if (msg.method == 'updatePopup') {
switch (msg.reason) { switch (msg.reason) {
case 'styleAdded': case 'styleAdded':
@ -93,7 +91,7 @@ function initPopup(url) {
// For domain // For domain
const domains = getDomains(url); const domains = getDomains(url);
for (let domain of domains) { for (const domain of domains) {
// Don't include TLD // Don't include TLD
if (domains.length > 1 && !domain.includes('.')) { if (domains.length > 1 && !domain.includes('.')) {
continue; continue;
@ -122,12 +120,13 @@ function showStyles(styles) {
installed.innerHTML = template.noStyles.outerHTML; installed.innerHTML = template.noStyles.outerHTML;
} else { } else {
const enabledFirst = prefs.get('popup.enabledFirst'); const enabledFirst = prefs.get('popup.enabledFirst');
styles.sort((a, b) => styles.sort((a, b) => (
enabledFirst && a.enabled !== b.enabled enabledFirst && a.enabled !== b.enabled
? !(a.enabled < b.enabled) ? -1 : 1 ? !(a.enabled < b.enabled) ? -1 : 1
: a.name.localeCompare(b.name)); : a.name.localeCompare(b.name)
));
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
for (let style of styles) { for (const style of styles) {
fragment.appendChild(createStyleElement(style)); fragment.appendChild(createStyleElement(style));
} }
installed.appendChild(fragment); installed.appendChild(fragment);
@ -135,6 +134,8 @@ function showStyles(styles) {
} }
// silence the inapplicable warning for async code
/* eslint no-use-before-define: [2, {"functions": false, "classes": false}] */
function createStyleElement(style) { function createStyleElement(style) {
const entry = template.style.cloneNode(true); const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id); entry.setAttribute('style-id', style.id);
@ -206,7 +207,7 @@ class EntryOnClick {
function confirm(ok) { function confirm(ok) {
window.onkeydown = null; window.onkeydown = null;
animateElement(box, {className: 'lights-on'}) animateElement(box, {className: 'lights-on'})
.then(() => box.dataset.display = false); .then(() => (box.dataset.display = false));
if (ok) { if (ok) {
deleteStyle(id).then(() => { deleteStyle(id).then(() => {
// update view with 'No styles installed for this site' message // update view with 'No styles installed for this site' message
@ -279,7 +280,7 @@ function handleUpdate(style) {
function handleDelete(id) { function handleDelete(id) {
var styleElement = $(`[style-id="${id}"]`, installed); const styleElement = $(`[style-id="${id}"]`, installed);
if (styleElement) { if (styleElement) {
installed.removeChild(styleElement); installed.removeChild(styleElement);
} }

View File

@ -1,31 +1,32 @@
var webSqlStorage = { /* global getDatabase, reportError */
'use strict';
migrate: function() { const webSqlStorage = {
if (typeof openDatabase == "undefined") {
migrate() {
if (typeof openDatabase == 'undefined') {
// No WebSQL - no migration! // No WebSQL - no migration!
return; return;
} }
webSqlStorage.getStyles(function(styles) { webSqlStorage.getStyles(styles => {
getDatabase(function(db) { getDatabase(db => {
var tx = db.transaction(["styles"], "readwrite"); const tx = db.transaction(['styles'], 'readwrite');
var os = tx.objectStore("styles"); const os = tx.objectStore('styles');
styles.forEach(function(s) { styles.forEach(s => {
webSqlStorage.cleanStyle(s) webSqlStorage.cleanStyle(s);
os.add(s); os.add(s);
}); });
// While this was running, the styles were loaded from the (empty) indexed db // While this was running, the styles were loaded from the (empty) indexed db
setTimeout(function() { setTimeout(() => invalidateCache(true), 500);
invalidateCache(true);
}, 500);
}); });
}, null); }, null);
}, },
cleanStyle: function(s) { cleanStyle(s) {
delete s.id; delete s.id;
s.sections.forEach(function(section) { s.sections.forEach(section => {
delete section.id; delete section.id;
["urls", "urlPrefixes", "domains", "regexps"].forEach(function(property) { ['urls', 'urlPrefixes', 'domains', 'regexps'].forEach(property => {
if (!section[property]) { if (!section[property]) {
section[property] = []; section[property] = [];
} }
@ -33,48 +34,66 @@ var webSqlStorage = {
}); });
}, },
getStyles: function(callback) { getStyles(callback) {
webSqlStorage.getDatabase(function(db) { webSqlStorage.getDatabase(db => {
if (!db) { if (!db) {
callback([]); callback([]);
return; return;
} }
db.readTransaction(function (t) { db.readTransaction(t => {
var where = ""; const where = '';
var params = []; const params = [];
t.executeSql('SELECT DISTINCT s.*, se.id section_id, se.code, sm.name metaName, sm.value metaValue FROM styles s LEFT JOIN sections se ON se.style_id = s.id LEFT JOIN section_meta sm ON sm.section_id = se.id WHERE 1' + where + ' ORDER BY s.id, se.id, sm.id', params, function (t, r) { t.executeSql(
var styles = []; 'SELECT DISTINCT ' +
var currentStyle = null; 's.*, se.id section_id, se.code, sm.name metaName, sm.value metaValue ' +
var currentSection = null; 'FROM styles s ' +
for (var i = 0; i < r.rows.length; i++) { 'LEFT JOIN sections se ON se.style_id = s.id ' +
var values = r.rows.item(i); 'LEFT JOIN section_meta sm ON sm.section_id = se.id ' +
var metaName = null; 'WHERE 1' + where + ' ' +
'ORDER BY s.id, se.id, sm.id',
params,
(t, r) => {
const styles = [];
let currentStyle = null;
let currentSection = null;
for (let i = 0; i < r.rows.length; i++) {
const values = r.rows.item(i);
let metaName = null;
switch (values.metaName) { switch (values.metaName) {
case null: case null:
break; break;
case "url": case 'url':
metaName = "urls"; metaName = 'urls';
break; break;
case "url-prefix": case 'url-prefix':
metaName = "urlPrefixes"; metaName = 'urlPrefixes';
break; break;
case "domain": case 'domain':
var metaName = "domains"; metaName = 'domains';
break; break;
case "regexps": case 'regexps':
var metaName = "regexps"; metaName = 'regexps';
break; break;
default: default:
var metaName = values.metaName + "s"; metaName = values.metaName + 's';
} }
var metaValue = values.metaValue; const metaValue = values.metaValue;
if (currentStyle == null || currentStyle.id != values.id) { if (currentStyle === null || currentStyle.id != values.id) {
currentStyle = {id: values.id, url: values.url, updateUrl: values.updateUrl, md5Url: values.md5Url, name: values.name, enabled: values.enabled == "true", originalMd5: values.originalMd5, sections: []}; currentStyle = {
id: values.id,
url: values.url,
updateUrl: values.updateUrl,
md5Url: values.md5Url,
name: values.name,
enabled: values.enabled == 'true',
originalMd5: values.originalMd5,
sections: []
};
styles.push(currentStyle); styles.push(currentStyle);
} }
if (values.section_id != null) { if (values.section_id !== null) {
if (currentSection == null || currentSection.id != values.section_id) { if (currentSection === null || currentSection.id != values.section_id) {
currentSection = {id: values.section_id, code: values.code}; currentSection = {id: values.section_id, code: values.code};
currentStyle.sections.push(currentSection); currentStyle.sections.push(currentSection);
} }
@ -93,77 +112,130 @@ var webSqlStorage = {
}, reportError); }, reportError);
}, },
getDatabase: function(ready, error) { getDatabase(ready, error) {
let stylishDb;
try { try {
stylishDb = openDatabase('stylish', '', 'Stylish Styles', 5 * 1024 * 1024); stylishDb = openDatabase('stylish', '', 'Stylish Styles', 5 * 1024 * 1024);
} catch (ex) { } catch (ex) {
error(); error();
throw ex; throw ex;
} }
if (stylishDb.version == "") { if (stylishDb.version == '') {
// It didn't already exist, we have nothing to migrate. // It didn't already exist, we have nothing to migrate.
ready(null); ready(null);
return; return;
} }
if (stylishDb.version == "1.0") { switch (stylishDb.version) {
webSqlStorage.dbV11(stylishDb, error, ready); case '1.0': return webSqlStorage.dbV11(stylishDb, error, ready);
} else if (stylishDb.version == "1.1") { case '1.1': return webSqlStorage.dbV12(stylishDb, error, ready);
webSqlStorage.dbV12(stylishDb, error, ready); case '1.2': return webSqlStorage.dbV13(stylishDb, error, ready);
} else if (stylishDb.version == "1.2") { case '1.3': return webSqlStorage.dbV14(stylishDb, error, ready);
webSqlStorage.dbV13(stylishDb, error, ready); case '1.4': return webSqlStorage.dbV15(stylishDb, error, ready);
} else if (stylishDb.version == "1.3") { default: ready(stylishDb);
webSqlStorage.dbV14(stylishDb, error, ready);
} else if (stylishDb.version == "1.4") {
webSqlStorage.dbV15(stylishDb, error, ready);
} else {
ready(stylishDb);
} }
}, },
dbV11: function(d, error, done) { dbV11(d, error, done) {
d.changeVersion(d.version, '1.1', function (t) { d.changeVersion(d.version, '1.1', t => {
t.executeSql('CREATE TABLE styles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, code TEXT NOT NULL, enabled INTEGER NOT NULL, originalCode TEXT NULL);'); t.executeSql(
t.executeSql('CREATE TABLE style_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);'); 'CREATE TABLE styles (' +
'id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' +
'url TEXT, ' +
'updateUrl TEXT, ' +
'md5Url TEXT, ' +
'name TEXT NOT NULL, ' +
'code TEXT NOT NULL, ' +
'enabled INTEGER NOT NULL, ' +
'originalCode TEXT NULL);');
t.executeSql(
'CREATE TABLE style_meta (' +
'id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' +
'style_id INTEGER NOT NULL, ' +
'name TEXT NOT NULL, ' +
'value TEXT NOT NULL);');
t.executeSql('CREATE INDEX style_meta_style_id ON style_meta (style_id);'); t.executeSql('CREATE INDEX style_meta_style_id ON style_meta (style_id);');
}, error, function() { webSqlStorage.dbV12(d, error, done)}); }, error, () => webSqlStorage.dbV12(d, error, done));
}, },
dbV12: function(d, error, done) { dbV12(d, error, done) {
d.changeVersion(d.version, '1.2', function (t) { d.changeVersion(d.version, '1.2', t => {
// add section table // add section table
t.executeSql('CREATE TABLE sections (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, code TEXT NOT NULL);'); t.executeSql(
t.executeSql('INSERT INTO sections (style_id, code) SELECT id, code FROM styles;'); 'CREATE TABLE sections (' +
'id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' +
'style_id INTEGER NOT NULL, ' +
'code TEXT NOT NULL);');
t.executeSql(
'INSERT INTO sections (style_id, code) SELECT id, code FROM styles;');
// switch meta to sections // switch meta to sections
t.executeSql('DROP INDEX style_meta_style_id;'); t.executeSql(
t.executeSql('CREATE TABLE section_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, section_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);'); 'DROP INDEX style_meta_style_id;');
t.executeSql('INSERT INTO section_meta (section_id, name, value) SELECT s.id, sm.name, sm.value FROM sections s INNER JOIN style_meta sm ON sm.style_id = s.style_id;'); t.executeSql(
t.executeSql('CREATE INDEX section_meta_section_id ON section_meta (section_id);'); 'CREATE TABLE section_meta (' +
t.executeSql('DROP TABLE style_meta;'); 'id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' +
'section_id INTEGER NOT NULL, ' +
'name TEXT NOT NULL, ' +
'value TEXT NOT NULL);');
t.executeSql(
'INSERT INTO section_meta (section_id, name, value) ' +
'SELECT s.id, sm.name, sm.value FROM sections s ' +
'INNER JOIN style_meta sm ON sm.style_id = s.style_id;');
t.executeSql(
'CREATE INDEX section_meta_section_id ON section_meta (section_id);');
t.executeSql(
'DROP TABLE style_meta;');
// drop extra fields from styles table // drop extra fields from styles table
t.executeSql('CREATE TABLE newstyles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, enabled INTEGER NOT NULL);'); t.executeSql(
t.executeSql('INSERT INTO newstyles (id, url, updateUrl, md5Url, name, enabled) SELECT id, url, updateUrl, md5Url, name, enabled FROM styles;'); 'CREATE TABLE newstyles (' +
t.executeSql('DROP TABLE styles;'); 'id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' +
t.executeSql('ALTER TABLE newstyles RENAME TO styles;'); 'url TEXT, ' +
}, error, function() { webSqlStorage.dbV13(d, error, done)}); 'updateUrl TEXT, ' +
'md5Url TEXT, ' +
'name TEXT NOT NULL, ' +
'enabled INTEGER NOT NULL);');
t.executeSql(
'INSERT INTO newstyles (id, url, updateUrl, md5Url, name, enabled) ' +
'SELECT id, url, updateUrl, md5Url, name, enabled FROM styles;');
t.executeSql(
'DROP TABLE styles;');
t.executeSql(
'ALTER TABLE newstyles ' +
'RENAME TO styles;');
}, error, () => webSqlStorage.dbV13(d, error, done));
}, },
dbV13: function(d, error, done) { dbV13(d, error, done) {
d.changeVersion(d.version, '1.3', function (t) { d.changeVersion(d.version, '1.3', t => {
// clear out orphans // clear out orphans
t.executeSql('DELETE FROM section_meta WHERE section_id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);'); t.executeSql(
t.executeSql('DELETE FROM sections WHERE id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);'); 'DELETE FROM section_meta ' +
}, error, function() { webSqlStorage.dbV14(d, error, done)}); 'WHERE section_id IN (' +
'SELECT sections.id FROM sections ' +
'LEFT JOIN styles ON styles.id = sections.style_id ' +
'WHERE styles.id IS NULL' +
');');
t.executeSql(
'DELETE FROM sections ' +
'WHERE id IN (' +
'SELECT sections.id FROM sections ' +
'LEFT JOIN styles ON styles.id = sections.style_id ' +
'WHERE styles.id IS NULL);');
}, error, () => webSqlStorage.dbV14(d, error, done));
}, },
dbV14: function(d, error, done) { dbV14(d, error, done) {
d.changeVersion(d.version, '1.4', function (t) { d.changeVersion(d.version, '1.4', t => {
t.executeSql('UPDATE styles SET url = null WHERE url = "undefined";'); t.executeSql(
}, error, function() { webSqlStorage.dbV15(d, error, done)}); 'UPDATE styles SET url = null ' +
'WHERE url = "undefined";');
}, error, () => webSqlStorage.dbV15(d, error, done));
}, },
dbV15: function(d, error, done) { dbV15(d, error, done) {
d.changeVersion(d.version, '1.5', function (t) { d.changeVersion(d.version, '1.5', t => {
t.executeSql('ALTER TABLE styles ADD COLUMN originalMd5 TEXT NULL;'); t.executeSql(
}, error, function() { done(d); }); 'ALTER TABLE styles ' +
} 'ADD COLUMN originalMd5 TEXT NULL;');
}, error, () => done(d));
} }
};

View File

@ -1,26 +1,38 @@
/* global cachedStyles: true, prefs: true, contextMenus: false */
/* global handleUpdate, handleDelete */
/* global webSqlStorage */
'use strict';
function getDatabase(ready, error) { function getDatabase(ready, error) {
var dbOpenRequest = window.indexedDB.open("stylish", 2); const dbOpenRequest = window.indexedDB.open('stylish', 2);
dbOpenRequest.onsuccess = function(e) { dbOpenRequest.onsuccess = event => {
ready(e.target.result); ready(event.target.result);
}; };
dbOpenRequest.onerror = function(event) { dbOpenRequest.onerror = event => {
console.log(event.target.errorCode); console.warn(event.target.errorCode);
if (error) { if (error) {
error(event); error(event);
} }
}; };
dbOpenRequest.onupgradeneeded = function(event) { dbOpenRequest.onupgradeneeded = event => {
if (event.oldVersion == 0) { if (event.oldVersion == 0) {
var os = event.target.result.createObjectStore("styles", {keyPath: 'id', autoIncrement: true}); event.target.result.createObjectStore('styles', {
keyPath: 'id',
autoIncrement: true,
});
webSqlStorage.migrate(); webSqlStorage.migrate();
} }
}
}; };
}
// Let manage/popup/edit reuse background page variables // Let manage/popup/edit reuse background page variables
// Note, only "var"-declared variables are visible from another extension page // Note, only 'var'-declared variables are visible from another extension page
var cachedStyles = ((bg) => bg && bg.cachedStyles || { // eslint-disable-next-line no-var
var cachedStyles, prefs;
(() => {
const bg = chrome.extension.getBackgroundPage();
cachedStyles = bg && bg.cachedStyles || {
bg, bg,
list: null, list: null,
noCode: null, noCode: null,
@ -30,7 +42,9 @@ var cachedStyles = ((bg) => bg && bg.cachedStyles || {
inProgress: false, inProgress: false,
onDone: [], onDone: [],
}, },
})(chrome.extension.getBackgroundPage()); };
prefs = bg && bg.prefs;
})();
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage // in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
@ -63,7 +77,7 @@ function getStyles(options, callback) {
} }
cachedStyles.mutex.inProgress = true; cachedStyles.mutex.inProgress = true;
const t0 = performance.now() //const t0 = performance.now();
getDatabase(db => { getDatabase(db => {
const tx = db.transaction(['styles'], 'readonly'); const tx = db.transaction(['styles'], 'readonly');
const os = tx.objectStore('styles'); const os = tx.objectStore('styles');
@ -71,20 +85,16 @@ function getStyles(options, callback) {
cachedStyles.list = event.target.result || []; cachedStyles.list = event.target.result || [];
cachedStyles.noCode = []; cachedStyles.noCode = [];
cachedStyles.byId.clear(); cachedStyles.byId.clear();
for (let style of cachedStyles.list) { for (const style of cachedStyles.list) {
const noCode = getStyleWithNoCode(style); const noCode = getStyleWithNoCode(style);
cachedStyles.noCode.push(noCode); cachedStyles.noCode.push(noCode);
cachedStyles.byId.set(style.id, {style, noCode}); cachedStyles.byId.set(style.id, {style, noCode});
} }
//console.log('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cachedStyles.mutex.onDone.map(e => JSON.stringify(e.options))) //console.log('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cachedStyles.mutex.onDone.map(e => JSON.stringify(e.options)))
try{ runTryCatch(callback, filterStyles(options));
callback(filterStyles(options));
} catch(e){
// no error in console, it works
}
cachedStyles.mutex.inProgress = false; cachedStyles.mutex.inProgress = false;
for (let {options, callback} of cachedStyles.mutex.onDone) { for (const {options, callback} of cachedStyles.mutex.onDone) {
callback(filterStyles(options)); callback(filterStyles(options));
} }
cachedStyles.mutex.onDone = []; cachedStyles.mutex.onDone = [];
@ -95,7 +105,7 @@ function getStyles(options, callback) {
function getStyleWithNoCode(style) { function getStyleWithNoCode(style) {
const stripped = Object.assign({}, style, {sections: []}); const stripped = Object.assign({}, style, {sections: []});
for (let section of style.sections) { for (const section of style.sections) {
stripped.sections.push(Object.assign({}, section, {code: null})); stripped.sections.push(Object.assign({}, section, {code: null}));
} }
return stripped; return stripped;
@ -153,7 +163,7 @@ function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
function filterStyles(options = {}) { function filterStyles(options = {}) {
const t0 = performance.now() //const t0 = performance.now();
const enabled = fixBoolean(options.enabled); const enabled = fixBoolean(options.enabled);
const url = 'url' in options ? options.url : null; const url = 'url' in options ? options.url : null;
const id = 'id' in options ? Number(options.id) : null; const id = 'id' in options ? Number(options.id) : null;
@ -161,14 +171,17 @@ function filterStyles(options = {}) {
const code = 'code' in options ? options.code : true; const code = 'code' in options ? options.code : true;
const asHash = 'asHash' in options ? options.asHash : false; const asHash = 'asHash' in options ? options.asHash : false;
if (enabled == null if (enabled === null
&& url == null && url === null
&& id == null && id === null
&& matchUrl == null && matchUrl === null
&& asHash != true) { && asHash != true) {
//console.log('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) //console.log('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options))
return code ? cachedStyles.list : cachedStyles.noCode; return code ? cachedStyles.list : cachedStyles.noCode;
} }
// silence the inapplicable warning for async code
// eslint-disable-next-line no-use-before-define
const disableAll = asHash && prefs.get('disableAll', false);
// add \t after url to prevent collisions (not sure it can actually happen though) // add \t after url to prevent collisions (not sure it can actually happen though)
const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + code + asHash; const cacheKey = ' ' + enabled + url + '\t' + id + matchUrl + '\t' + code + asHash;
@ -177,12 +190,13 @@ function filterStyles(options = {}) {
//console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) //console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options))
cached.hits++; cached.hits++;
cached.lastHit = Date.now(); cached.lastHit = Date.now();
return asHash return asHash
? Object.assign({disableAll: prefs.get('disableAll', false)}, cached.styles) ? Object.assign({disableAll}, cached.styles)
: cached.styles; : cached.styles;
} }
const styles = id == null const styles = id === null
? (code ? cachedStyles.list : cachedStyles.noCode) ? (code ? cachedStyles.list : cachedStyles.noCode)
: [(cachedStyles.byId.get(id) || {})[code ? 'style' : 'noCode']]; : [(cachedStyles.byId.get(id) || {})[code ? 'style' : 'noCode']];
const filtered = asHash ? {} : []; const filtered = asHash ? {} : [];
@ -192,15 +206,15 @@ function filterStyles(options = {}) {
return filtered; return filtered;
} }
for (let i = 0, style; (style = styles[i]); i++) { for (let i = 0, style; (style = styles[i]); i++) {
if ((enabled == null || style.enabled == enabled) if ((enabled === null || style.enabled == enabled)
&& (url == null || style.url == url) && (url === null || style.url == url)
&& (id == null || style.id == id)) { && (id === null || style.id == id)) {
const sections = (asHash || matchUrl != null) && getApplicableSections(style, matchUrl); const sections = (asHash || matchUrl !== null) && getApplicableSections(style, matchUrl);
if (asHash) { if (asHash) {
if (sections.length) { if (sections.length) {
filtered[style.id] = sections; filtered[style.id] = sections;
} }
} else if (matchUrl == null || sections.length) { } else if (matchUrl === null || sections.length) {
filtered.push(style); filtered.push(style);
} }
} }
@ -215,7 +229,7 @@ function filterStyles(options = {}) {
cleanupCachedFilters(); cleanupCachedFilters();
} }
return asHash return asHash
? Object.assign({disableAll: prefs.get('disableAll', false)}, filtered) ? Object.assign({disableAll}, filtered)
: filtered; : filtered;
} }
@ -235,7 +249,7 @@ function cleanupCachedFilters({force = false} = {}) {
const hitWeight = 1 / 4; // we make ~4 hits per URL const hitWeight = 1 / 4; // we make ~4 hits per URL
const lastHitWeight = 10; const lastHitWeight = 10;
// delete the oldest 10% // delete the oldest 10%
const sorted = [...cachedStyles.filters.entries()] [...cachedStyles.filters.entries()]
.map(([id, v], index) => ({ .map(([id, v], index) => ({
id, id,
weight: weight:
@ -267,10 +281,10 @@ function saveStyle(style) {
} }
// Update // Update
if (id != null) { if (id !== null) {
style.id = id; style.id = id;
os.get(id).onsuccess = eventGet => { os.get(id).onsuccess = eventGet => {
const existed = !!eventGet.target.result; const existed = Boolean(eventGet.target.result);
const oldStyle = Object.assign({}, eventGet.target.result); const oldStyle = Object.assign({}, eventGet.target.result);
const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle); const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle);
style = Object.assign(oldStyle, style); style = Object.assign(oldStyle, style);
@ -343,7 +357,7 @@ function deleteStyle(id, {notify = true} = {}) {
getDatabase(db => { getDatabase(db => {
const tx = db.transaction(['styles'], 'readwrite'); const tx = db.transaction(['styles'], 'readwrite');
const os = tx.objectStore('styles'); const os = tx.objectStore('styles');
os.delete(Number(id)).onsuccess = event => { os.delete(Number(id)).onsuccess = () => {
invalidateCache(notify, {deletedId: id}); invalidateCache(notify, {deletedId: id});
if (notify) { if (notify) {
notifyAllTabs({method: 'styleDeleted', id}); notifyAllTabs({method: 'styleDeleted', id});
@ -357,32 +371,31 @@ function deleteStyle(id, {notify = true} = {}) {
} }
function reportError() { function reportError(...args) {
for (i in arguments) { for (const arg of args) {
if ("message" in arguments[i]) { if ('message' in arg) {
//alert(arguments[i].message); console.log(arg.message);
console.log(arguments[i].message);
} }
} }
} }
function fixBoolean(b) { function fixBoolean(b) {
if (typeof b != "undefined") { if (typeof b != 'undefined') {
return b != "false"; return b != 'false';
} }
return null; return null;
} }
function getDomains(url) { function getDomains(url) {
if (url.indexOf("file:") == 0) { if (url.indexOf('file:') == 0) {
return []; return [];
} }
var d = /.*?:\/*([^\/:]+)/.exec(url)[1]; let d = /.*?:\/*([^/:]+)/.exec(url)[1];
var domains = [d]; const domains = [d];
while (d.indexOf(".") != -1) { while (d.indexOf('.') != -1) {
d = d.substring(d.indexOf(".") + 1); d = d.substring(d.indexOf('.') + 1);
domains.push(d); domains.push(d);
} }
return domains; return domains;
@ -406,7 +419,7 @@ function getType(o) {
const namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/; const namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/;
function getApplicableSections(style, url) { function getApplicableSections(style, url) {
var sections = style.sections.filter(function(section) { const sections = style.sections.filter(function(section) {
return sectionAppliesToUrl(section, url); return sectionAppliesToUrl(section, url);
}); });
// ignore if it's just namespaces // ignore if it's just namespaces
@ -418,116 +431,87 @@ function getApplicableSections(style, url) {
function sectionAppliesToUrl(section, url) { function sectionAppliesToUrl(section, url) {
// only http, https, file, and chrome-extension allowed // only http, https, file, ftp, and chrome-extension://OWN_EXTENSION_ID allowed
if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) { if (!url.startsWith('http')
&& !url.startsWith('ftp')
&& !url.startsWith('file')
&& !url.startsWith(OWN_ORIGIN)) {
return false; return false;
} }
// other extensions can't be styled if (section.urls.length == 0
if (url.indexOf("chrome-extension") == 0 && url.indexOf(chrome.extension.getURL("")) != 0) { && section.domains.length == 0
return false; && section.urlPrefixes.length == 0
} && section.regexps.length == 0) {
if (section.urls.length == 0 && section.domains.length == 0 && section.urlPrefixes.length == 0 && section.regexps.length == 0) {
//console.log(section.id + " is global");
return true; return true;
} }
if (section.urls.indexOf(url) != -1) { if (section.urls.indexOf(url) != -1) {
//console.log(section.id + " applies to " + url + " due to URL rules");
return true; return true;
} }
if (section.urlPrefixes.some(function(prefix) { for (const urlPrefix of section.urlPrefixes) {
return url.indexOf(prefix) == 0; if (url.startsWith(urlPrefix)) {
})) {
//console.log(section.id + " applies to " + url + " due to URL prefix rules");
return true; return true;
} }
if (section.domains.length > 0 && getDomains(url).some(function(domain) { }
return section.domains.indexOf(domain) != -1; if (section.domains.length) {
})) { for (const domain of getDomains(url)) {
//console.log(section.id + " applies due to " + url + " due to domain rules"); if (section.domains.indexOf(domain) != -1) {
return true; return true;
} }
if (section.regexps.some(function(regexp) { }
}
for (const regexp of section.regexps) {
// we want to match the full url, so add ^ and $ if not already present // we want to match the full url, so add ^ and $ if not already present
if (regexp[0] != "^") { const prefix = regexp.charAt(0) != '^' && '^';
regexp = "^" + regexp; const suffix = regexp.slice(-1) != '$' && '$';
} const re = runTryCatch(() => new RegExp(prefix + regexp + suffix));
if (regexp[regexp.length - 1] != "$") { if (!re) {
regexp += "$"; console.warn('Regexp ' + regexp + ' is not valid');
} } else if (re.test(url)) {
var re = runTryCatch(function() { return new RegExp(regexp) });
if (re) {
return (re).test(url);
} else {
console.log(section.id + "'s regexp '" + regexp + "' is not valid");
}
})) {
//console.log(section.id + " applies to " + url + " due to regexp rules");
return true; return true;
} }
//console.log(section.id + " does not apply due to " + url); }
return false; return false;
} }
function isCheckbox(el) { function isCheckbox(el) {
return el.nodeName.toLowerCase() == "input" && "checkbox" == el.type.toLowerCase(); return el.localName == 'input' && el.type == 'checkbox';
} }
// js engine can't optimize the entire function if it contains try-catch // 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 // so we should keep it isolated from normal code in a minimal wrapper
function runTryCatch(func) { // Update: might get fixed in V8 TurboFan in the future
try { return func() } function runTryCatch(func, ...args) {
catch(e) {} try {
return func(...args);
} catch (e) {}
} }
// Accepts an array of pref names (values are fetched via prefs.get) prefs = prefs || new function Prefs() {
// and establishes a two-way connection between the document elements and the actual prefs const me = this;
function setupLivePrefs(IDs) {
var 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(function(request) {
if (request.prefName in localIDs) {
updateElement(request.prefName);
}
});
function updateElement(id) {
var el = document.getElementById(id);
el[isCheckbox(el) ? "checked" : "value"] = prefs.get(id);
el.dispatchEvent(new Event("change", {bubbles: true, cancelable: true}));
return el;
}
}
var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() { const defaults = {
var me = this; '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
var defaults = { 'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
"openEditInWindow": false, // new editor opens in a own browser window 'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
"windowPosition": {}, // detached window position 'popup.enabledFirst': true, // display enabled styles before disabled styles
"show-badge": true, // display text on popup menu icon 'popup.stylesFirst': true, // display enabled styles before disabled styles
"disableAll": false, // boss key
"popup.breadcrumbs": true, // display "New style" links as URL breadcrumbs 'manage.onlyEnabled': false, // display only enabled styles
"popup.breadcrumbs.usePath": false, // use URL path for "this URL" 'manage.onlyEdited': false, // display only styles created locally
"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 'editor.options': {}, // CodeMirror.defaults.*
"manage.onlyEdited": false, // display only styles created locally 'editor.lineWrapping': true, // word wrap
'editor.smartIndent': true, // 'smart' indent
"editor.options": {}, // CodeMirror.defaults.* 'editor.indentWithTabs': false, // smart indent with tabs
"editor.lineWrapping": true, // word wrap 'editor.tabSize': 4, // tab width, in spaces
"editor.smartIndent": true, // "smart" indent 'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default',
"editor.indentWithTabs": false, // smart indent with tabs 'editor.theme': 'default', // CSS theme
"editor.tabSize": 4, // tab width, in spaces 'editor.beautify': { // CSS beautifier
"editor.keyMap": navigator.appVersion.indexOf("Windows") > 0 ? "sublime" : "default",
"editor.theme": "default", // CSS theme
"editor.beautify": { // CSS beautifier
selector_separator_newline: true, selector_separator_newline: true,
newline_before_open_brace: false, newline_before_open_brace: false,
newline_after_open_brace: true, newline_after_open_brace: true,
@ -536,21 +520,21 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
newline_between_rules: false, newline_between_rules: false,
end_with_newline: false end_with_newline: false
}, },
"editor.lintDelay": 500, // lint gutter marker update delay, ms 'editor.lintDelay': 500, // lint gutter marker update delay, ms
"editor.lintReportDelay": 4500, // lint report update delay, ms 'editor.lintReportDelay': 4500, // lint report update delay, ms
"badgeDisabled": "#8B0000", // badge background color when disabled 'badgeDisabled': '#8B0000', // badge background color when disabled
"badgeNormal": "#006666", // badge background color 'badgeNormal': '#006666', // badge background color
"popupWidth": 240, // popup width in pixels 'popupWidth': 240, // popup width in pixels
"updateInterval": 0 // user-style automatic update interval, hour 'updateInterval': 0 // user-style automatic update interval, hour
}; };
var values = deepCopy(defaults); const values = deepCopy(defaults);
var syncTimeout; // see broadcast() function below let syncTimeout; // see broadcast() function below
Object.defineProperty(this, "readOnlyValues", {value: {}}); Object.defineProperty(this, 'readOnlyValues', {value: {}});
Prefs.prototype.get = function(key, defaultValue) { Prefs.prototype.get = function(key, defaultValue) {
if (key in values) { if (key in values) {
@ -565,12 +549,12 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
console.warn("No default preference for '%s'", key); console.warn("No default preference for '%s'", key);
}; };
Prefs.prototype.getAll = function(key) { Prefs.prototype.getAll = function() {
return deepCopy(values); return deepCopy(values);
}; };
Prefs.prototype.set = function(key, value, options) { Prefs.prototype.set = function(key, value, options) {
var oldValue = deepCopy(values[key]); const oldValue = deepCopy(values[key]);
values[key] = value; values[key] = value;
defineReadonlyProperty(this.readOnlyValues, key, value); defineReadonlyProperty(this.readOnlyValues, key, value);
if ((!options || !options.noBroadcast) && !equal(value, oldValue)) { if ((!options || !options.noBroadcast) && !equal(value, oldValue)) {
@ -578,19 +562,19 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
} }
}; };
Prefs.prototype.remove = function(key) { me.set(key, undefined) }; Prefs.prototype.remove = key => me.set(key, undefined);
Prefs.prototype.broadcast = function(key, value, options) { Prefs.prototype.broadcast = function(key, value, options) {
var message = {method: "prefChanged", prefName: key, value: value}; const message = {method: 'prefChanged', prefName: key, value: value};
notifyAllTabs(message); notifyAllTabs(message);
chrome.runtime.sendMessage(message); chrome.runtime.sendMessage(message);
if (key == "disableAll") { if (key == 'disableAll') {
notifyAllTabs({method: "styleDisableAll", disableAll: value}); notifyAllTabs({method: 'styleDisableAll', disableAll: value});
} }
if (!options || !options.noSync) { if (!options || !options.noSync) {
clearTimeout(syncTimeout); clearTimeout(syncTimeout);
syncTimeout = setTimeout(function() { syncTimeout = setTimeout(function() {
getSync().set({"settings": values}); getSync().set({'settings': values});
}, 0); }, 0);
} }
}; };
@ -599,20 +583,20 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
me.set(key, defaults[key], {noBroadcast: true}); me.set(key, defaults[key], {noBroadcast: true});
}); });
getSync().get("settings", function(result) { getSync().get('settings', function(result) {
var synced = result.settings; const synced = result.settings;
for (var key in defaults) { for (const key in defaults) {
if (synced && (key in synced)) { if (synced && (key in synced)) {
me.set(key, synced[key], {noSync: true}); me.set(key, synced[key], {noSync: true});
} else { } else {
var value = tryMigrating(key); const value = tryMigrating(key);
if (value !== undefined) { if (value !== undefined) {
me.set(key, value); me.set(key, value);
} }
} }
} }
if (typeof contextMenus !== 'undefined') { if (typeof contextMenus !== 'undefined') {
for (let id in contextMenus) { for (const id in contextMenus) {
if (typeof values[id] == 'boolean') { if (typeof values[id] == 'boolean') {
me.broadcast(id, values[id], {noSync: true}); me.broadcast(id, values[id], {noSync: true});
} }
@ -621,17 +605,17 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
}); });
chrome.storage.onChanged.addListener(function(changes, area) { chrome.storage.onChanged.addListener(function(changes, area) {
if (area == "sync" && "settings" in changes) { if (area == 'sync' && 'settings' in changes) {
var synced = changes.settings.newValue; const synced = changes.settings.newValue;
if (synced) { if (synced) {
for (key in defaults) { for (const key in defaults) {
if (key in synced) { if (key in synced) {
me.set(key, synced[key], {noSync: true}); me.set(key, synced[key], {noSync: true});
} }
} }
} else { } else {
// user manually deleted our settings, we'll recreate them // user manually deleted our settings, we'll recreate them
getSync().set({"settings": values}); getSync().set({'settings': values});
} }
} }
}); });
@ -640,15 +624,15 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
if (!(key in localStorage)) { if (!(key in localStorage)) {
return undefined; return undefined;
} }
var value = localStorage[key]; const value = localStorage[key];
delete localStorage[key]; delete localStorage[key];
localStorage["DEPRECATED: " + key] = value; localStorage['DEPRECATED: ' + key] = value;
switch (typeof defaults[key]) { switch (typeof defaults[key]) {
case "boolean": case 'boolean':
return value.toLowerCase() === "true"; return value.toLowerCase() === 'true';
case "number": case 'number':
return Number(value); return Number(value);
case "object": case 'object':
try { try {
return JSON.parse(value); return JSON.parse(value);
} catch (e) { } catch (e) {
@ -658,19 +642,43 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
} }
return value; return value;
} }
}; }();
// 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(function(request) {
if (request.prefName in localIDs) {
updateElement(request.prefName);
}
});
function updateElement(id) {
const el = document.getElementById(id);
el[isCheckbox(el) ? 'checked' : 'value'] = prefs.get(id);
el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
return el;
}
}
function getCodeMirrorThemes(callback) { function getCodeMirrorThemes(callback) {
chrome.runtime.getPackageDirectoryEntry(function(rootDir) { chrome.runtime.getPackageDirectoryEntry(function(rootDir) {
rootDir.getDirectory("codemirror/theme", {create: false}, function(themeDir) { rootDir.getDirectory('codemirror/theme', {create: false}, function(themeDir) {
themeDir.createReader().readEntries(function(entries) { themeDir.createReader().readEntries(function(entries) {
var themes = [chrome.i18n.getMessage("defaultTheme")]; const themes = [chrome.i18n.getMessage('defaultTheme')];
entries entries
.filter(function(entry) { return entry.isFile }) .filter(entry => entry.isFile)
.sort(function(a, b) { return a.name < b.name ? -1 : 1 }) .sort((a, b) => (a.name < b.name ? -1 : 1))
.forEach(function(entry) { .forEach(function(entry) {
themes.push(entry.name.replace(/\.css$/, "")); themes.push(entry.name.replace(/\.css$/, ''));
}); });
if (callback) { if (callback) {
callback(themes); callback(themes);
@ -682,40 +690,44 @@ function getCodeMirrorThemes(callback) {
function sessionStorageHash(name) { function sessionStorageHash(name) {
var hash = { return {
value: {}, name,
set: function(k, v) { this.value[k] = v; this.updateStorage(); }, value: runTryCatch(JSON.parse, sessionStorage[name]) || {},
unset: function(k) { delete this.value[k]; this.updateStorage(); }, set(k, v) {
updateStorage: function() { this.value[k] = v;
this.updateStorage();
},
unset(k) {
delete this.value[k];
this.updateStorage();
},
updateStorage() {
sessionStorage[this.name] = JSON.stringify(this.value); sessionStorage[this.name] = JSON.stringify(this.value);
} }
}; };
try { hash.value = JSON.parse(sessionStorage[name]); } catch(e) {}
Object.defineProperty(hash, "name", {value: name});
return hash;
} }
function deepCopy(obj) { function deepCopy(obj) {
if (!obj || typeof obj != "object") { if (!obj || typeof obj != 'object') {
return obj; return obj;
} else { } else {
var emptyCopy = Object.create(Object.getPrototypeOf(obj)); const emptyCopy = Object.create(Object.getPrototypeOf(obj));
return deepMerge(emptyCopy, obj); return deepMerge(emptyCopy, obj);
} }
} }
function deepMerge(target, obj1 /* plus any number of object arguments */) { function deepMerge(target, ...args) {
for (var i = 1; i < arguments.length; i++) { for (const obj of args) {
var obj = arguments[i]; for (const k in obj) {
for (var k in obj) { const value = obj[k];
// hasOwnProperty checking is not needed for our non-OOP stuff if (!value || typeof value != 'object') {
var value = obj[k];
if (!value || typeof value != "object") {
target[k] = value; target[k] = value;
} else if (k in target) { } else if (k in target) {
deepMerge(target[k], value); deepMerge(target[k], value);
} else if (typeof value.slice == 'function') {
target[k] = value.slice();
} else { } else {
target[k] = deepCopy(value); target[k] = deepCopy(value);
} }
@ -726,13 +738,13 @@ function deepMerge(target, obj1 /* plus any number of object arguments */) {
function equal(a, b) { function equal(a, b) {
if (!a || !b || typeof a != "object" || typeof b != "object") { if (!a || !b || typeof a != 'object' || typeof b != 'object') {
return a === b; return a === b;
} }
if (Object.keys(a).length != Object.keys(b).length) { if (Object.keys(a).length != Object.keys(b).length) {
return false; return false;
} }
for (var k in a) { for (const k in a) {
if (a[k] !== b[k]) { if (a[k] !== b[k]) {
return false; return false;
} }
@ -742,33 +754,32 @@ function equal(a, b) {
function defineReadonlyProperty(obj, key, value) { function defineReadonlyProperty(obj, key, value) {
var copy = deepCopy(value); const copy = deepCopy(value);
// In ES6, freezing a literal is OK (it returns the same value), but in previous versions it's an exception. if (typeof copy == 'object') {
if (typeof copy == "object") {
Object.freeze(copy); Object.freeze(copy);
} }
Object.defineProperty(obj, key, {value: copy, configurable: true}) Object.defineProperty(obj, key, {value: copy, configurable: true});
} }
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 // Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
function getSync() { function getSync() {
if ("sync" in chrome.storage) { if ('sync' in chrome.storage) {
return chrome.storage.sync; return chrome.storage.sync;
} }
crappyStorage = {}; const crappyStorage = {};
return { return {
get: function(key, callback) { get(key, callback) {
callback(crappyStorage[key] || {}); callback(crappyStorage[key] || {});
}, },
set: function(source, callback) { set(source, callback) {
for (var property in source) { for (const property in source) {
if (source.hasOwnProperty(property)) { if (source.hasOwnProperty(property)) {
crappyStorage[property] = source[property]; crappyStorage[property] = source[property];
} }
} }
callback(); callback();
} }
} };
} }
@ -781,15 +792,19 @@ function styleSectionsEqual(styleA, styleB) {
} }
const propNames = ['code', 'urlPrefixes', 'urls', 'domains', 'regexps']; const propNames = ['code', 'urlPrefixes', 'urls', 'domains', 'regexps'];
const typeBcaches = []; const typeBcaches = [];
checkingEveryInA: for (let sectionA of styleA.sections) { checkingEveryInA:
for (const sectionA of styleA.sections) {
const typeAcache = new Map(); const typeAcache = new Map();
for (let name of propNames) { for (const name of propNames) {
typeAcache.set(name, getType(sectionA[name])); typeAcache.set(name, getType(sectionA[name]));
} }
lookingForDupeInB: for (let i = 0, sectionB; (sectionB = styleB.sections[i]); i++) { lookingForDupeInB:
for (let i = 0, sectionB; (sectionB = styleB.sections[i]); i++) {
const typeBcache = typeBcaches[i] = typeBcaches[i] || new Map(); const typeBcache = typeBcaches[i] = typeBcaches[i] || new Map();
comparingProps: for (let name of propNames) { comparingProps:
const propA = sectionA[name], typeA = typeAcache.get(name); for (const name of propNames) {
const propA = sectionA[name];
const typeA = typeAcache.get(name);
const propB = sectionB[name]; const propB = sectionB[name];
let typeB = typeBcache.get(name); let typeB = typeBcache.get(name);
if (!typeB) { if (!typeB) {
@ -813,7 +828,7 @@ function styleSectionsEqual(styleA, styleB) {
if (propA.length != propB.length) { if (propA.length != propB.length) {
continue lookingForDupeInB; continue lookingForDupeInB;
} }
for (let item of propA) { for (const item of propA) {
if (propB.indexOf(item) < 0) { if (propB.indexOf(item) < 0) {
continue lookingForDupeInB; continue lookingForDupeInB;
} }

View File

@ -1,6 +1,7 @@
/* globals getStyles, saveStyle, prefs */ /* globals getStyles */
'use strict'; 'use strict';
// TODO: refactor to make usable in manage::Updater
var update = { var update = {
fetch: (resource, callback) => { fetch: (resource, callback) => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
@ -30,6 +31,7 @@ var update = {
getStyles({}, (styles) => callback(styles.filter(style => style.updateUrl))); getStyles({}, (styles) => callback(styles.filter(style => style.updateUrl)));
}, },
perform: (observe = function () {}) => { perform: (observe = function () {}) => {
// TODO: use sectionsAreEqual
// from install.js // from install.js
function arraysAreEqual (a, b) { function arraysAreEqual (a, b) {
// treat empty array and undefined as equivalent // treat empty array and undefined as equivalent