diff --git a/.eslintrc b/.eslintrc index a646543e..797e53a7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,52 +1,62 @@ # https://github.com/eslint/eslint/blob/master/docs/rules/README.md parserOptions: - ecmaVersion: 2017 + ecmaVersion: 2015 env: browser: true - commonjs: true es6: true webextensions: true globals: - CodeMirror: false - runTryCatch: true - getStylesSafe: true - getStyles: true - updateIcon: true - saveStyle: true - invalidateCache: true - getDatabase: true + # messaging.js + OWN_ORIGIN: false + KEEP_CHANNEL_OPEN: false + configureCommands: false + notifyAllTabs: false + refreshAllTabs: false + updateIcon: false + 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 - reportError: true - getActiveTab: true - t: true - getCodeMirrorThemes: true - setupLivePrefs: true - sessionStorageHash: true - template: true - tE: true - tHTML: true - CSSLint: true - enableStyle: true - deleteStyle: true - getType: true - importStyles: true - getActiveTabRealURL: true - openURL: true - $: true - $$: true - animateElement: true - scrollElementIntoView: true - getClickedStyleElement: true - getClickedStyleId: true - onDOMready: true - getDomains: true - webSqlStorage: true - notifyAllTabs: true - handleUpdate: true - handleDelete: true + cachedStyles: false + sessionStorageHash: false + getStylesSafe: false + invalidateCache: false + saveStyle: false + enableStyle: false + deleteStyle: false + fixBoolean: false + getDomains: false + getType: false + getApplicableSections: false + isCheckbox: false + runTryCatch: false + setupLivePrefs: false + getCodeMirrorThemes: false + styleSectionsEqual: false rules: accessor-pairs: [2] @@ -56,7 +66,7 @@ rules: arrow-parens: [2, as-needed] arrow-spacing: [2, {before: true, after: true}] block-scoped-var: [2] - brace-style: [2, 1tbs, {allowSingleLine: true}] + brace-style: [2, 1tbs, {allowSingleLine: false}] camelcase: [2, {properties: never}] class-methods-use-this: [2] comma-dangle: [0] @@ -77,16 +87,15 @@ rules: func-names: [0] generator-star-spacing: [2, before] global-require: [0] - guard-for-in: [2] + guard-for-in: [0] # not needed for our non-OOP stuff handle-callback-err: [2, ^(err|error)$] id-blacklist: [0] id-length: [0] id-match: [0] - indent: [2, 2, {VariableDeclarator: 0}] + indent: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}] jsx-quotes: [0] key-spacing: [0] keyword-spacing: [2] - linebreak-style: [2, unix] lines-around-comment: [0] lines-around-directive: [0] max-len: [2, {code: 120, ignoreComments: true, ignoreRegExpLiterals: true}] @@ -107,7 +116,7 @@ rules: no-case-declarations: [2] no-class-assign: [2] no-cond-assign: [2, except-parens] - no-confusing-arrow: [2] + no-confusing-arrow: [2, {allowParens: true}] no-const-assign: [2] no-constant-condition: [0] no-continue: [0] @@ -134,11 +143,11 @@ rules: no-extra-label: [0] no-extra-parens: [0] no-extra-semi: [2] - no-fallthrough: [2] + no-fallthrough: [2, {commentPattern: fallthrough.*}] no-floating-decimal: [0] no-func-assign: [2] no-global-assign: [2] - no-implicit-coercion: [2] + no-implicit-coercion: [1] no-implicit-globals: [0] no-implied-eval: [2] no-inline-comments: [0] @@ -148,7 +157,7 @@ rules: no-irregular-whitespace: [2] no-iterator: [2] no-label-var: [2] - no-labels: [2] + no-labels: [2, {allowLoop: true}] no-lone-blocks: [2] no-lonely-if: [0] no-loop-func: [0] @@ -158,7 +167,7 @@ rules: no-mixed-spaces-and-tabs: [2] no-multi-spaces: [0] 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-negated-condition: [0] no-negated-in-lhs: [2] @@ -193,7 +202,7 @@ rules: no-tabs: [2] no-template-curly-in-string: [2] no-this-before-super: [2] - no-throw-literal: [2] + no-throw-literal: [0] no-trailing-spaces: [2] no-undef-init: [2] no-undef: [2] @@ -207,29 +216,30 @@ rules: no-unsafe-negation: [2] no-unused-expressions: [2] 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-useless-call: [2] no-useless-computed-key: [2] no-useless-concat: [2] no-useless-constructor: [2] no-useless-escape: [2] - no-var: [0] + no-var: [1] no-warning-comments: [0] no-whitespace-before-property: [2] no-with: [2] object-curly-newline: [0] object-curly-spacing: [2, never] object-shorthand: [0] - one-var-declaration-per-line: [0] + one-var-declaration-per-line: [1] one-var: [0] operator-assignment: [2, always] - operator-linebreak: [2, after] + operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}] padded-blocks: [2, never] prefer-numeric-literals: [2] prefer-rest-params: [0] + prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}] quote-props: [0] - quotes: [2, double, avoid-escape] + quotes: [1, single, avoid-escape] radix: [2, as-needed] require-jsdoc: [0] require-yield: [2] @@ -242,7 +252,7 @@ rules: space-in-parens: [2, never] space-infix-ops: [2] space-unary-ops: [2] - spaced-comment: [2, always, {markers: ["!"]}] + spaced-comment: [0, always, {markers: ["!"]}] strict: [2, global] symbol-description: [2] template-curly-spacing: [2, never] diff --git a/apply.js b/apply.js index 9456dc36..d54497ee 100644 --- a/apply.js +++ b/apply.js @@ -1,373 +1,404 @@ -// using ES5 syntax because ES6 is fast only since around Chrome 55 -// so we'll wait until Chrome 60 arguably before converting +// Not using some slow features of ES6, see http://kpdecker.github.io/six-speed/ +// like destructring, classes, defaults, spread, calculated key names +/* eslint no-var: 0 */ +'use strict'; -var g_disableAll = false; -var g_styleElements = {}; -var iframeObserver; +var disableAll = false; +var styleElements = new Map(); var retiredStyleIds = []; +var iframeObserver; initObserver(); requestStyles(); - -function requestStyles(options = {}) { - // If this is a Stylish page (Edit Style or Manage Styles), - // we'll request the styles directly to minimize delay and flicker, - // 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.) - var request = Object.assign({ - method: "getStyles", - matchUrl: location.href, - enabled: true, - asHash: true, - }, options); - if (typeof getStylesSafe !== 'undefined') { - getStylesSafe(request).then(applyStyles); - } else { - chrome.runtime.sendMessage(request, applyStyles); - } -} - chrome.runtime.onMessage.addListener(applyOnMessage); + +function requestStyles(options) { + // If this is a Stylish page (Edit Style or Manage Styles), + // we'll request the styles directly to minimize delay and flicker, + // 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.) + const request = Object.assign({ + method: 'getStyles', + matchUrl: location.href, + enabled: true, + asHash: true, + }, options); + if (typeof getStylesSafe !== 'undefined') { + getStylesSafe(request).then(applyStyles); + } else { + chrome.runtime.sendMessage(request, applyStyles); + } +} + + function applyOnMessage(request, sender, sendResponse) { - // Also handle special request just for the pop-up - switch (request.method == "updatePopup" ? request.reason : request.method) { - case "styleDeleted": - removeStyle(request.id, document); - break; - case "styleUpdated": - if (request.codeIsUpdated === false) { - applyStyleState(request.style.id, request.style.enabled, document); - break; - } - if (request.style.enabled) { - retireStyle(request.style.id); - // fallthrough to "styleAdded" - } else { - removeStyle(request.style.id, document); - break; - } - case "styleAdded": - if (request.style.enabled) { - chrome.runtime.sendMessage({method: "getStyles", matchUrl: location.href, enabled: true, id: request.style.id, asHash: true}, applyStyles); - } - break; - case "styleApply": - applyStyles(request.styles); - break; - case "styleReplaceAll": - replaceAll(request.styles, document); - break; - case "styleDisableAll": - disableAll(request.disableAll); - break; - case "ping": - sendResponse(true); - break; - } + // Also handle special request just for the pop-up + switch (request.method == 'updatePopup' ? request.reason : request.method) { + + case 'styleDeleted': + removeStyle(request.id, document); + break; + + case 'styleUpdated': + if (request.codeIsUpdated === false) { + applyStyleState(request.style.id, request.style.enabled, document); + break; + } + if (!request.style.enabled) { + removeStyle(request.style.id, document); + break; + } + retireStyle(request.style.id); + // fallthrough to 'styleAdded' + + case 'styleAdded': + if (request.style.enabled) { + requestStyles({id: request.style.id}, applyStyles); + } + break; + + case 'styleApply': + applyStyles(request.styles); + break; + + case 'styleReplaceAll': + replaceAll(request.styles, document); + break; + + case 'styleDisableAll': + doDisableAll(request.disableAll); + break; + + case 'ping': + sendResponse(true); + break; + } } -function disableAll(disable) { - if (!disable === !g_disableAll) { - return; - } - g_disableAll = disable; - if (g_disableAll) { - iframeObserver.disconnect(); - } - disableSheets(g_disableAll, document); +function doDisableAll(disable) { + if (!disable === !disableAll) { + return; + } + disableAll = disable; + if (disableAll) { + iframeObserver.disconnect(); + } - if (!g_disableAll && document.readyState != "loading") { - iframeObserver.start(); - } + disableSheets(disableAll, document); - function disableSheets(disable, doc) { - Array.prototype.forEach.call(doc.styleSheets, function(stylesheet) { - if (stylesheet.ownerNode.classList.contains("stylus") - && stylesheet.disabled != disable) { - stylesheet.disabled = disable; - } - }); - getDynamicIFrames(doc).forEach(function(iframe) { - if (!disable) { - // update the IFRAME if it was created while the observer was disconnected - addDocumentStylesToIFrame(iframe); - } - disableSheets(disable, iframe.contentDocument); - }); - } + if (!disableAll && document.readyState != 'loading') { + iframeObserver.start(); + } + + function disableSheets(disable, doc) { + Array.prototype.forEach.call(doc.styleSheets, stylesheet => { + if (stylesheet.ownerNode.classList.contains('stylus') + && stylesheet.disabled != disable) { + stylesheet.disabled = disable; + } + }); + for (const iframe of getDynamicIFrames(doc)) { + if (!disable) { + // update the IFRAME if it was created while the observer was disconnected + addDocumentStylesToIFrame(iframe); + } + disableSheets(disable, iframe.contentDocument); + } + } } + function applyStyleState(id, enabled, doc) { - var e = doc.getElementById("stylus-" + id); - if (!e) { - if (enabled) { - requestStyles({id}); - } - } else { - e.sheet.disabled = !enabled; - getDynamicIFrames(doc).forEach(function(iframe) { - applyStyleState(id, iframe.contentDocument); - }); - } + const el = doc.getElementById('stylus-' + id); + if (el) { + el.sheet.disabled = !enabled; + processDynamicIFrames(doc, applyStyleState, id, enabled); + } else if (enabled) { + requestStyles({id}); + } } + function removeStyle(id, doc) { - var e = doc.getElementById("stylus-" + id); - delete g_styleElements["stylus-" + id]; - if (e) { - e.remove(); - } - if (doc == document && Object.keys(g_styleElements).length == 0) { - iframeObserver.disconnect(); - } - getDynamicIFrames(doc).forEach(function(iframe) { - removeStyle(id, iframe.contentDocument); - }); + styleElements.delete('stylus-' + id); + const el = doc.getElementById('stylus-' + id); + if (el) { + el.remove(); + } + if (doc == document && !styleElements.size) { + iframeObserver.disconnect(); + } + processDynamicIFrames(doc, removeStyle, id); } + // to avoid page flicker when the style is updated // 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 function retireStyle(id, doc) { - var deadID = "ghost-" + id; - if (!doc) { - doc = document; - retiredStyleIds.push(deadID); - delete g_styleElements["stylus-" + id]; - // in case something went wrong and new style was never applied - setTimeout(removeStyle.bind(null, deadID, doc), 1000); - } - var e = doc.getElementById("stylus-" + id); - if (e) { - e.id = "stylus-" + deadID; - } - getDynamicIFrames(doc).forEach(function(iframe) { - retireStyle(id, iframe.contentDocument); - }); + const deadID = 'ghost-' + id; + if (!doc) { + doc = document; + retiredStyleIds.push(deadID); + styleElements.delete('stylus-' + id); + // in case something went wrong and new style was never applied + setTimeout(removeStyle, 1000, deadID, doc); + } + const el = doc.getElementById('stylus-' + id); + if (el) { + el.id = 'stylus-' + deadID; + } + processDynamicIFrames(doc, retireStyle, id); } + function applyStyles(styleHash) { - if (!styleHash) { // Chrome is starting up - requestStyles(); - return; - } - if ("disableAll" in styleHash) { - disableAll(styleHash.disableAll); - delete styleHash.disableAll; - } + if (!styleHash) { // Chrome is starting up + requestStyles(); + return; + } + if ('disableAll' in styleHash) { + doDisableAll(styleHash.disableAll); + delete styleHash.disableAll; + } - for (var styleId in styleHash) { - applySections(styleId, styleHash[styleId]); - } + for (const styleId in styleHash) { + applySections(styleId, styleHash[styleId]); + } - if (Object.keys(g_styleElements).length) { - // 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 - // (which already is autogenerated at this moment for the xml response) - if (document.head && document.head.firstChild && document.head.firstChild.id == "xml-viewer-style") { - for (var id in g_styleElements) { - document.head.appendChild(document.getElementById(id)); - } - } - document.addEventListener("DOMContentLoaded", onDOMContentLoaded); - } + if (styleElements.size) { + // 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 + // which is already autogenerated at this moment + if (document.head && document.head.firstChild && document.head.firstChild.id == 'xml-viewer-style') { + for (const id of styleElements.keys()) { + document.head.appendChild(document.getElementById(id)); + } + } + document.addEventListener('DOMContentLoaded', onDOMContentLoaded); + } - if (retiredStyleIds.length) { - setTimeout(function() { - while (retiredStyleIds.length) { - removeStyle(retiredStyleIds.shift(), document); - } - }, 0); - } + if (retiredStyleIds.length) { + setTimeout(function() { + while (retiredStyleIds.length) { + removeStyle(retiredStyleIds.shift(), document); + } + }, 0); + } } + function onDOMContentLoaded() { - addDocumentStylesToAllIFrames(); - iframeObserver.start(); + addDocumentStylesToAllIFrames(); + iframeObserver.start(); } + function applySections(styleId, sections) { - var styleElement = document.getElementById("stylus-" + styleId); - // Already there. - if (styleElement) { - return; - } - if (document.documentElement instanceof SVGSVGElement) { - // SVG document, make an SVG style element. - styleElement = document.createElementNS("http://www.w3.org/2000/svg", "style"); - } else { - // 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"); - } - styleElement.setAttribute("id", "stylus-" + styleId); - styleElement.setAttribute("class", "stylus"); - styleElement.setAttribute("type", "text/css"); - styleElement.appendChild(document.createTextNode(sections.map(function(section) { - return section.code; - }).join("\n"))); - addStyleElement(styleElement, document); - g_styleElements[styleElement.id] = styleElement; + let el = document.getElementById('stylus-' + styleId); + // Already there. + if (el) { + return; + } + if (document.documentElement instanceof SVGSVGElement) { + // SVG document, make an SVG style element. + el = document.createElementNS('http://www.w3.org/2000/svg', 'style'); + } else { + // This will make an HTML style element. If there's SVG embedded in an HTML document, this works on the SVG too. + el = document.createElement('style'); + } + el.setAttribute('id', 'stylus-' + styleId); + el.setAttribute('class', 'stylus'); + el.setAttribute('type', 'text/css'); + el.appendChild(document.createTextNode(sections.map(section => section.code).join('\n'))); + addStyleElement(el, document); + styleElements.set(el.id, el); } -function addStyleElement(styleElement, doc) { - if (!doc.documentElement || doc.getElementById(styleElement.id)) { - return; - } - doc.documentElement.appendChild(doc.importNode(styleElement, true)) - .disabled = g_disableAll; - getDynamicIFrames(doc).forEach(function(iframe) { - if (iframeIsLoadingSrcDoc(iframe)) { - addStyleToIFrameSrcDoc(iframe, styleElement); - } else { - addStyleElement(styleElement, iframe.contentDocument); - } - }); + +function addStyleElement(el, doc) { + if (!doc.documentElement || doc.getElementById(el.id)) { + return; + } + doc.documentElement.appendChild(doc.importNode(el, true)) + .disabled = disableAll; + for (const iframe of getDynamicIFrames(doc)) { + if (iframeIsLoadingSrcDoc(iframe)) { + addStyleToIFrameSrcDoc(iframe, el); + } else { + addStyleElement(el, iframe.contentDocument); + } + } } + function addDocumentStylesToIFrame(iframe) { - var doc = iframe.contentDocument; - var srcDocIsLoading = iframeIsLoadingSrcDoc(iframe); - for (var id in g_styleElements) { - if (srcDocIsLoading) { - addStyleToIFrameSrcDoc(iframe, g_styleElements[id]); - } else { - addStyleElement(g_styleElements[id], doc); - } - } + const doc = iframe.contentDocument; + const srcDocIsLoading = iframeIsLoadingSrcDoc(iframe); + for (const el of styleElements.values()) { + if (srcDocIsLoading) { + addStyleToIFrameSrcDoc(iframe, el); + } else { + addStyleElement(el, doc); + } + } } + 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. function getDynamicIFrames(doc) { - return Array.prototype.filter.call(doc.getElementsByTagName('iframe'), iframeIsDynamic); + return [...doc.getElementsByTagName('iframe')].filter(iframeIsDynamic); } + function iframeIsDynamic(f) { - var href; - try { - href = f.contentDocument.location.href; - } catch (ex) { - // Cross-origin, so it's not a dynamic iframe - return false; - } - return href == document.location.href || href.indexOf("about:") == 0; + let href; + if (f.src && f.src.startsWith('http') && new URL(f.src).origin != location.origin) { + return false; + } + try { + href = f.contentDocument.location.href; + } catch (ex) { + // Cross-origin, so it's not a dynamic iframe + return false; + } + 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) { - 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' + 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' } -function addStyleToIFrameSrcDoc(iframe, styleElement) { - if (g_disableAll) { - return; - } - iframe.srcdoc += styleElement.outerHTML; - // make sure the style is added in case srcdoc was malformed - setTimeout(addStyleElement.bind(null, styleElement, iframe.contentDocument), 100); + +function addStyleToIFrameSrcDoc(iframe, el) { + if (disableAll) { + return; + } + iframe.srcdoc += el.outerHTML; + // make sure the style is added in case srcdoc was malformed + setTimeout(addStyleElement, 100, el, iframe.contentDocument); } -function replaceAll(newStyles, doc, pass2) { - var oldStyles = [].slice.call(doc.querySelectorAll("STYLE.stylus" + (pass2 ? "[id$='-ghost']" : ""))); - if (!pass2) { - oldStyles.forEach(function(style) { style.id += "-ghost"; }); - } - getDynamicIFrames(doc).forEach(function(iframe) { - replaceAll(newStyles, iframe.contentDocument, pass2); - }); - if (doc == document && !pass2) { - g_styleElements = {}; - applyStyles(newStyles); - replaceAll(newStyles, doc, true); - } - if (pass2) { - oldStyles.forEach(function(style) { style.remove(); }); - } + +function replaceAll(newStyles, doc) { + const oldStyles = [...doc.querySelectorAll('STYLE.stylus')]; + oldStyles.forEach(style => (style.id += '-ghost')); + processDynamicIFrames(doc, replaceAll, newStyles); + if (doc == document) { + styleElements.clear(); + applyStyles(newStyles); + replaceAllpass2(newStyles, doc); + } } + +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 function initObserver() { - var orphanCheckTimer; + let orphanCheckTimer; - iframeObserver = new MutationObserver(function(mutations) { - clearTimeout(orphanCheckTimer); - // MutationObserver runs as a microtask so the timer won't fire until all queued mutations are fired - orphanCheckTimer = setTimeout(orphanCheck, 0); + iframeObserver = new MutationObserver(function(mutations) { + clearTimeout(orphanCheckTimer); + // MutationObserver runs as a microtask so the timer won't fire until all queued mutations are fired + orphanCheckTimer = setTimeout(orphanCheck, 0); - if (mutations.length > 1000) { - // use a much faster method for very complex pages with 100,000 mutations - // (observer usually receives 1k-10k mutations per call) - addDocumentStylesToAllIFrames(); - return; - } - // move the check out of current execution context - // because some same-domain (!) iframes fail to load when their "contentDocument" is accessed (!) - // namely gmail's old chat iframe talkgadget.google.com - setTimeout(process.bind(null, mutations), 0); - }); + if (mutations.length > 1000) { + // use a much faster method for very complex pages with 100,000 mutations + // (observer usually receives 1k-10k mutations per call) + addDocumentStylesToAllIFrames(); + return; + } + // move the check out of current execution context + // because some same-domain (!) iframes fail to load when their 'contentDocument' is accessed (!) + // namely gmail's old chat iframe talkgadget.google.com + setTimeout(process, 0, mutations); + }); - function process(mutations) { - for (var m = 0, ml = mutations.length; m < ml; m++) { - var mutation = mutations[m]; - if (mutation.type === "childList") { - for (var n = 0, nodes = mutation.addedNodes, nl = nodes.length; n < nl; n++) { - var node = nodes[n]; - if (node.localName === "iframe" && iframeIsDynamic(node)) { - addDocumentStylesToIFrame(node); - } - } - } - } - } + 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++) { + const mutation = mutations[m]; + if (mutation.type === 'childList') { + // eslint-disable-next-line no-var + for (var n = 0, nodes = mutation.addedNodes, nl = nodes.length; n < nl; n++) { + const node = nodes[n]; + if (node.localName === 'iframe' && iframeIsDynamic(node)) { + addDocumentStylesToIFrame(node); + } + } + } + } + } - iframeObserver.start = function() { - // will be ignored by browser if already observing - iframeObserver.observe(document, {childList: true, subtree: true}); - } + iframeObserver.start = () => { + // subsequent calls are ignored if already started observing + iframeObserver.observe(document, {childList: true, subtree: true}); + }; - function orphanCheck() { - orphanCheckTimer = 0; - var port = chrome.runtime.connect(); - if (port) { - port.disconnect(); - return; - } + function orphanCheck() { + orphanCheckTimer = 0; + const port = chrome.runtime.connect(); + if (port) { + port.disconnect(); + return; + } - // we're orphaned due to an extension update - // we can detach the mutation observer - iframeObserver.takeRecords(); - iframeObserver.disconnect(); - iframeObserver = null; - // we can detach event listeners - document.removeEventListener("DOMContentLoaded", onDOMContentLoaded); - // we can't detach chrome.runtime.onMessage because it's no longer connected internally + // we're orphaned due to an extension update + // we can detach the mutation observer + iframeObserver.takeRecords(); + iframeObserver.disconnect(); + iframeObserver = null; + // we can detach event listeners + document.removeEventListener('DOMContentLoaded', onDOMContentLoaded); + // 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 - [ - 'addDocumentStylesToAllIFrames', - 'addDocumentStylesToIFrame', - 'addStyleElement', - 'addStyleToIFrameSrcDoc', - 'applyOnMessage', - 'applySections', - 'applyStyles', - 'disableAll', - 'getDynamicIFrames', - 'iframeIsDynamic', - 'iframeIsLoadingSrcDoc', - 'initObserver', - 'removeStyle', - 'replaceAll', - 'requestStyles', - 'retireStyle' - ].forEach(fn => window[fn] = null); + // we can destroy global functions in this context to free up memory + [ + 'addDocumentStylesToAllIFrames', + 'addDocumentStylesToIFrame', + 'addStyleElement', + 'addStyleToIFrameSrcDoc', + 'applyOnMessage', + 'applySections', + 'applyStyles', + 'doDisableAll', + 'getDynamicIFrames', + 'processDynamicIFrames', + 'iframeIsDynamic', + 'iframeIsLoadingSrcDoc', + 'initObserver', + 'removeStyle', + 'replaceAll', + 'replaceAllpass2', + 'requestStyles', + 'retireStyle' + ].forEach(fn => (window[fn] = null)); - // we can destroy global variables - g_styleElements = iframeObserver = retiredStyleIds = null; - } + // we can destroy global variables + styleElements = iframeObserver = retiredStyleIds = null; + } } diff --git a/background.js b/background.js index 80d2a1b5..988f1dec 100644 --- a/background.js +++ b/background.js @@ -1,43 +1,60 @@ -/* 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 // why the content script also asks for this stuff. -chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, 'styleApply')); -chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, 'styleReplaceAll')); -chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null)); +chrome.webNavigation.onCommitted.addListener(data => { + webNavigationListener('styleApply', data); +}); +chrome.webNavigation.onHistoryStateUpdated.addListener(data => { + webNavigationListener('styleReplaceAll', data); +}); +chrome.webNavigation.onBeforeNavigate.addListener(data => { + webNavigationListener(null, data); +}); + function webNavigationListener(method, data) { - getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => { - // 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 - if (method && !data.url.startsWith('chrome')) { - chrome.tabs.sendMessage(data.tabId, {method, styles}, {frameId: data.frameId}); - } - // main page frame id is 0 - if (data.frameId == 0) { - updateIcon({id: data.tabId, url: data.url}, styles); - } - }); + getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => { + // 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 + if (method && !data.url.startsWith('chrome')) { + chrome.tabs.sendMessage( + data.tabId, + {method, styles}, + {frameId: data.frameId}); + } + // main page frame id is 0 + if (data.frameId == 0) { + updateIcon({id: data.tabId, url: data.url}, styles); + } + }); } // catch direct URL hash modifications not invoked via HTML5 history API +const tabUrlHasHash = new Set(); -var tabUrlHasHash = {}; -chrome.tabs.onUpdated.addListener(function(tabId, info, tab) { - if (info.status == "loading" && info.url) { - if (info.url.indexOf('#') > 0) { - tabUrlHasHash[tabId] = true; - } else if (tabUrlHasHash[tabId]) { - delete tabUrlHasHash[tabId]; - } else { - // do nothing since the tab neither had # before nor has # now - return; - } - webNavigationListener("styleReplaceAll", {tabId: tabId, frameId: 0, url: info.url}); - } +chrome.tabs.onUpdated.addListener((tabId, info, tab) => { + if (info.status != 'loading' || !info.url) { + return; + } + if (info.url.includes('#')) { + tabUrlHasHash.add(tabId); + } else if (tabUrlHasHash.has(tabId)) { + tabUrlHasHash.delete(tabId); + } else { + // do nothing since the tab neither had # before nor has # now + return; + } + 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 @@ -45,75 +62,78 @@ chrome.tabs.onRemoved.addListener(function(tabId, info) { chrome.runtime.onMessage.addListener(onBackgroundMessage); function onBackgroundMessage(request, sender, sendResponse) { - switch (request.method) { - case "getStyles": - var styles = getStyles(request, sendResponse); - // check if this is a main content frame style enumeration - if (request.matchUrl && !request.id - && sender && sender.tab && sender.frameId == 0 - && sender.tab.url == request.matchUrl) { - updateIcon(sender.tab, styles); - } - return KEEP_CHANNEL_OPEN; - case "saveStyle": - saveStyle(request).then(sendResponse); - return KEEP_CHANNEL_OPEN; - case "invalidateCache": - if (typeof invalidateCache != "undefined") { - invalidateCache(false, request); - } - break; - case "healthCheck": - getDatabase(function() { sendResponse(true); }, function() { sendResponse(false); }); - return KEEP_CHANNEL_OPEN; - case "openURL": - openURL(request); - break; - case "styleDisableAll": - // fallthru to prefChanged - request = {prefName: 'disableAll', value: request.disableAll}; - case "prefChanged": - if (typeof request.value == 'boolean' && contextMenus[request.prefName]) { - chrome.contextMenus.update(request.prefName, {checked: request.value}); - } - break; - case "refreshAllTabs": - refreshAllTabs().then(sendResponse); - return KEEP_CHANNEL_OPEN; - } + switch (request.method) { + + case 'getStyles': + var styles = getStyles(request, sendResponse); // eslint-disable-line no-var + // check if this is a main content frame style enumeration + if (request.matchUrl && !request.id + && sender && sender.tab && sender.frameId == 0 + && sender.tab.url == request.matchUrl) { + updateIcon(sender.tab, styles); + } + return KEEP_CHANNEL_OPEN; + + case 'saveStyle': + saveStyle(request).then(sendResponse); + return KEEP_CHANNEL_OPEN; + + case 'invalidateCache': + if (typeof invalidateCache != 'undefined') { + invalidateCache(false, request); + } + break; + + case 'healthCheck': + getDatabase( + () => sendResponse(true), + () => sendResponse(false)); + return KEEP_CHANNEL_OPEN; + + case 'styleDisableAll': + request = {prefName: 'disableAll', value: request.disableAll}; + // fallthrough to prefChanged + + case 'prefChanged': + // eslint-disable-next-line no-use-before-define + if (typeof request.value == 'boolean' && contextMenus[request.prefName]) { + chrome.contextMenus.update(request.prefName, {checked: request.value}); + } + break; + } } // commands (global hotkeys) const browserCommands = { - openManage() { - openURL({url: '/manage.html'}); - }, - styleDisableAll(state) { - prefs.set('disableAll', - typeof state == 'boolean' ? state : !prefs.get('disableAll')); - }, + openManage() { + openURL({url: '/manage.html'}); + }, + styleDisableAll(state) { + prefs.set('disableAll', + typeof state == 'boolean' ? state : !prefs.get('disableAll')); + }, }; // Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350 if ('commands' in chrome) { - chrome.commands.onCommand.addListener(command => browserCommands[command]()); + chrome.commands.onCommand.addListener(command => browserCommands[command]()); } // context menus const contextMenus = { - 'show-badge': { - title: 'menuShowBadge', - click: info => prefs.set(info.menuItemId, info.checked), - }, - 'disableAll': { - title: 'disableAllStyles', - click: browserCommands.styleDisableAll, - }, - 'open-manager': { - title: 'openStylesManager', - click: browserCommands.openManage, - }, + 'show-badge': { + title: 'menuShowBadge', + click: info => prefs.set(info.menuItemId, info.checked), + }, + 'disableAll': { + title: 'disableAllStyles', + click: browserCommands.styleDisableAll, + }, + 'open-manager': { + title: 'openStylesManager', + click: browserCommands.openManage, + }, }; // detect browsers without Delete by looking at the end of UA string @@ -121,35 +141,35 @@ const contextMenus = { // but skip CentBrowser: Safari/# plus Shockwave Flash in plugins // Vivaldi: Vivaldi/# if (/Vivaldi\/[\d.]+$/.test(navigator.userAgent) - || /Safari\/[\d.]+$/.test(navigator.userAgent) - && ![...navigator.plugins].some(p => p.name == 'Shockwave Flash')) { - contextMenus.editDeleteText = { - title: 'editDeleteText', - contexts: ['editable'], - documentUrlPatterns: [OWN_ORIGIN + 'edit*'], - click: (info, tab) => { - chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'}); - }, - }; + || /Safari\/[\d.]+$/.test(navigator.userAgent) + && ![...navigator.plugins].some(p => p.name == 'Shockwave Flash')) { + contextMenus.editDeleteText = { + title: 'editDeleteText', + contexts: ['editable'], + documentUrlPatterns: [OWN_ORIGIN + 'edit*'], + click: (info, tab) => { + chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'}); + }, + }; } chrome.contextMenus.onClicked.addListener((info, tab) => - contextMenus[info.menuItemId].click(info, tab)); + contextMenus[info.menuItemId].click(info, tab)); Object.keys(contextMenus).forEach(id => { - const item = Object.assign({id}, contextMenus[id]); - const prefValue = prefs.readOnlyValues[id]; - const isBoolean = typeof prefValue == 'boolean'; - item.title = chrome.i18n.getMessage(item.title); - if (isBoolean) { - item.type = 'checkbox'; - item.checked = prefValue; - } - if (!item.contexts) { - item.contexts = ['browser_action']; - } - delete item.click; - chrome.contextMenus.create(item, ignoreChromeError); + const item = Object.assign({id}, contextMenus[id]); + const prefValue = prefs.readOnlyValues[id]; + const isBoolean = typeof prefValue == 'boolean'; + item.title = chrome.i18n.getMessage(item.title); + if (isBoolean) { + item.type = 'checkbox'; + item.checked = prefValue; + } + if (!item.contexts) { + item.contexts = ['browser_action']; + } + delete item.click; + chrome.contextMenus.create(item, ignoreChromeError); }); @@ -159,75 +179,75 @@ getDatabase(function() {}, reportError); // When an edit page gets attached or detached, remember its state // so we can do the same to the next one to open. -var editFullUrl = chrome.extension.getURL("edit.html"); -chrome.tabs.onAttached.addListener(function(tabId, data) { - chrome.tabs.get(tabId, function(tabData) { - if (tabData.url.indexOf(editFullUrl) == 0) { - chrome.windows.get(tabData.windowId, {populate: true}, function(win) { - // If there's only one tab in this window, it's been dragged to new window - prefs.set("openEditInWindow", win.tabs.length == 1); - }); - } - }); +const editFullUrl = OWN_ORIGIN + 'edit.html'; +chrome.tabs.onAttached.addListener((tabId, data) => { + chrome.tabs.get(tabId, tabData => { + if (tabData.url.startsWith(editFullUrl)) { + chrome.windows.get(tabData.windowId, {populate: true}, win => { + // If there's only one tab in this window, it's been dragged to new window + prefs.set('openEditInWindow', win.tabs.length == 1); + }); + } + }); }); -var codeMirrorThemes; -getCodeMirrorThemes(function(themes) { - codeMirrorThemes = themes; -}); +var codeMirrorThemes; // eslint-disable-line no-var +getCodeMirrorThemes(themes => (codeMirrorThemes = themes)); // do not use prefs.get('version', null) as it might not yet be available chrome.storage.local.get('version', prefs => { - // Open FAQs page once after installation to guide new users, - // https://github.com/schomery/stylish-chrome/issues/22#issuecomment-279936160 - if (!prefs.version) { - // do not display the FAQs page in development mode - if ('update_url' in chrome.runtime.getManifest()) { - let version = chrome.runtime.getManifest().version; - chrome.storage.local.set({ - version - }, () => { - window.setTimeout(() => { - chrome.tabs.create({ - url: 'http://add0n.com/stylus.html?version=' + version + '&type=install' - }); - }, 3000); - }) - } - } + // Open FAQs page once after installation to guide new users, + // https://github.com/schomery/stylish-chrome/issues/22#issuecomment-279936160 + if (!prefs.version) { + // do not display the FAQs page in development mode + if ('update_url' in chrome.runtime.getManifest()) { + const version = chrome.runtime.getManifest().version; + chrome.storage.local.set({version}, () => { + window.setTimeout(() => { + chrome.tabs.create({ + url: `http://add0n.com/stylus.html?version=${version}&type=install` + }); + }, 3000); + }); + } + } }); + injectContentScripts(); function injectContentScripts() { - const contentScripts = chrome.app.getDetails().content_scripts; - for (let cs of contentScripts) { - cs.matches = cs.matches.map(m => m == '' ? m : wildcardAsRegExp(m)); - } - chrome.tabs.query({url: '*://*/*'}, tabs => { - for (let tab of tabs) { - for (let cs of contentScripts) { - for (let m of cs.matches) { - if (m == '' || tab.url.match(m)) { - chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => { - if (!pong) { - chrome.tabs.executeScript(tab.id, { - file: cs.js[0], - runAt: cs.run_at, - allFrames: cs.all_frames, - }, ignoreChromeError); - } - }); - // inject the content script just once - break; - } - } - } - } - }); + const contentScripts = chrome.app.getDetails().content_scripts; + for (const cs of contentScripts) { + cs.matches = cs.matches.map(m => ( + m == '' ? m : wildcardAsRegExp(m) + )); + } + // also inject in chrome://newtab/ page + chrome.tabs.query({url: '*://*/*'}, tabs => { + for (const tab of tabs) { + for (const cs of contentScripts) { + for (const m of cs.matches) { + if (m == '' || tab.url.match(m)) { + chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => { + if (!pong) { + chrome.tabs.executeScript(tab.id, { + file: cs.js[0], + runAt: cs.run_at, + allFrames: cs.all_frames, + }, ignoreChromeError); + } + }); + // inject the content script just once + break; + } + } + } + } + }); } function ignoreChromeError() { - chrome.runtime.lastError; + chrome.runtime.lastError; // eslint-disable-line no-unused-expressions } diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js index faa5d204..3bf03510 100644 --- a/backup/fileSaveLoad.js +++ b/backup/fileSaveLoad.js @@ -1,4 +1,3 @@ -/* globals getStyles, saveStyle, invalidateCache, refreshAllTabs */ 'use strict'; const STYLISH_DUMP_FILE_EXT = '.txt'; @@ -228,7 +227,7 @@ function importFromString(jsonString) { $('#file-all-styles').onclick = () => { - getStyles({}, function (styles) { + getStylesSafe().then(styles => { const text = JSON.stringify(styles, null, '\t'); const fileName = generateFileName(); diff --git a/edit.js b/edit.js index d8e8b076..6c44007c 100644 --- a/edit.js +++ b/edit.js @@ -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"; var styleId = null; @@ -29,7 +30,7 @@ Array.prototype.rotate = function(amount) { // negative amount == rotate left var r = this.slice(-amount, this.length); Array.prototype.push.apply(r, this.slice(0, this.length - r.length)); return r; -} +}; Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }}); diff --git a/health.js b/health.js index 727fa4ad..ac1dc96c 100644 --- a/health.js +++ b/health.js @@ -1,11 +1,14 @@ +'use strict'; + setTimeout(healthCheck, 0); function healthCheck() { - chrome.runtime.sendMessage({method: "healthCheck"}, function(ok) { - if (ok === undefined) { // Chrome is starting up - healthCheck(); - } else if (!ok && confirm(t("dbError"))) { - window.open("http://userstyles.org/dberror"); - } - }); + chrome.runtime.sendMessage({method: 'healthCheck'}, ok => { + if (ok === undefined) { + // Chrome is starting up + healthCheck(); + } else if (!ok && confirm(t('dbError'))) { + window.open('http://userstyles.org/dberror'); + } + }); } diff --git a/install.js b/install.js index cc1ffb7d..f8a74fb4 100644 --- a/install.js +++ b/install.js @@ -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) { if (response.length == 0) { sendEvent("styleCanBeInstalledChrome"); @@ -17,7 +20,7 @@ chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") } else { getResource(getMeta("stylish-code-chrome"), function(code) { // this would indicate a failure (a style with settings?). - if (code == null) { + if (code === null) { sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl}); } var json = JSON.parse(code); @@ -30,7 +33,7 @@ chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") // everything's the same sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl}); return; - }; + } } sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl}); }); @@ -49,10 +52,12 @@ function sectionsAreEqual(a, b) { function arraysAreEqual(a, b) { // treat empty array and undefined as equivalent - if (typeof a == "undefined") + if (typeof a == "undefined") { return (typeof b == "undefined") || (b.length == 0); - if (typeof b == "undefined") + } + if (typeof b == "undefined") { return (typeof a == "undefined") || (a.length == 0); + } if (a.length != b.length) { return false; } @@ -78,7 +83,7 @@ function stylishInstallChrome() { // check for old style json var json = JSON.parse(code); json.method = "saveStyle"; - chrome.runtime.sendMessage(json, function(response) { + chrome.runtime.sendMessage(json, function() { sendEvent("styleInstalledChrome"); }); }); @@ -90,7 +95,10 @@ function stylishInstallChrome() { document.addEventListener("stylishInstallChrome", stylishInstallChrome); function stylishUpdateChrome() { 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]; if (confirm(chrome.i18n.getMessage('styleUpdate', [style.name]))) { getResource(getMeta("stylish-code-chrome"), function(code) { @@ -123,14 +131,14 @@ function getResource(url, callback) { if (xhr.status >= 400) { callback(null); } else { - callback(xhr.responseText); + callback(xhr.responseText); } } - } + }; if (url.length > 2000) { var parts = url.split("?"); xhr.open("POST", parts[0], true); - xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.send(parts[1]); } else { xhr.open("GET", url, true); @@ -139,7 +147,7 @@ function getResource(url, callback) { } /* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */ -(function (es) { +(function(es) { es.forEach(e => { [...e.childNodes].filter(n => n.nodeType == 3).forEach(n => { n.nodeValue = n.nodeValue.replace('Stylish', 'Stylus'); @@ -176,5 +184,5 @@ function orphanCheck() { 'sendEvent', 'stylishUpdateChrome', 'stylishInstallChrome' - ].forEach(fn => window[fn] = null); + ].forEach(fn => (window[fn] = null)); } diff --git a/localization.js b/localization.js index e5104ed7..70489c02 100644 --- a/localization.js +++ b/localization.js @@ -1,91 +1,94 @@ -var template = {}; +'use strict'; + +const template = {}; tDocLoader(); + function t(key, params) { - var s = chrome.i18n.getMessage(key, params) - if (s == "") { - throw "Missing string '" + key + "'."; - } - return s; -} -function o(key) { - document.write(t(key)); + const s = chrome.i18n.getMessage(key, params); + if (s == '') { + throw `Missing string "${key}"`; + } + return s; } + + function tE(id, key, attr, esc) { - if (attr) { - document.getElementById(id).setAttribute(attr, t(key)); - } else if (typeof esc == "undefined" || esc) { - document.getElementById(id).appendChild(document.createTextNode(t(key))); - } else { - document.getElementById(id).innerHTML = t(key); - } + if (attr) { + document.getElementById(id).setAttribute(attr, t(key)); + } else if (typeof esc == 'undefined' || esc) { + document.getElementById(id).appendChild(document.createTextNode(t(key))); + } else { + document.getElementById(id).innerHTML = t(key); + } } + function tHTML(html) { - var node = document.createElement("div"); - node.innerHTML = html.replace(/>\s+<'); // spaces are removed; use   for an explicit space - tNodeList(node.querySelectorAll("*")); - var child = node.removeChild(node.firstElementChild); - node.remove(); - return child; + const node = document.createElement('div'); + node.innerHTML = html.replace(/>\s+<'); // spaces are removed; use   for an explicit space + if (html.includes('i18n-')) { + tNodeList(node.querySelectorAll('*')); + } + return node.firstElementChild; } + function tNodeList(nodes) { - for (var n = 0; n < nodes.length; n++) { - var node = nodes[n]; - if (node.nodeType != 1) { // not an ELEMENT_NODE - continue; - } - if (node.localName == "template") { - tNodeList(node.content.querySelectorAll("*")); - template[node.dataset.id] = node.content.firstElementChild; - continue; - } - for (var a = node.attributes.length - 1; a >= 0; a--) { - var attr = node.attributes[a]; - var name = attr.nodeName; - if (name.indexOf("i18n-") != 0) { - continue; - } - name = name.substr(5); // "i18n-".length - var value = t(attr.value); - switch (name) { - case "text": - node.insertBefore(document.createTextNode(value), node.firstChild); - break; - case "html": - node.insertAdjacentHTML("afterbegin", value); - break; - default: - node.setAttribute(name, value); - } - node.removeAttribute(attr.nodeName); - } - } + for (const node of [...nodes]) { + // skip non-ELEMENT_NODE + if (node.nodeType != 1) { + continue; + } + if (node.localName == 'template') { + tNodeList(node.content.querySelectorAll('*')); + template[node.dataset.id] = node.content.firstElementChild; + continue; + } + for (const attr of [...node.attributes]) { + let name = attr.nodeName; + if (name.indexOf('i18n-') != 0) { + continue; + } + name = name.substr(5); // 'i18n-'.length + const value = t(attr.value); + switch (name) { + case 'text': + node.insertBefore(document.createTextNode(value), node.firstChild); + break; + case 'html': + node.insertAdjacentHTML('afterbegin', value); + break; + default: + node.setAttribute(name, value); + } + node.removeAttribute(attr.nodeName); + } + } } + function tDocLoader() { - // localize HEAD - tNodeList(document.all); + // localize HEAD + tNodeList(document.all); - // localize BODY - const observer = new MutationObserver(function(mutations) { - for (var m = 0; m < mutations.length; m++) { - tNodeList(mutations[m].addedNodes); - } - }); - - const onLoad = () => { - tDocLoader.stop(); - tNodeList(document.all); - }; - tDocLoader.start = () => { - observer.observe(document, {subtree: true, childList: true}); - }; - tDocLoader.stop = () => { - observer.disconnect(); - document.removeEventListener('DOMContentLoaded', onLoad); - }; - tDocLoader.start(); - document.addEventListener('DOMContentLoaded', onLoad); + // localize BODY + const observer = new MutationObserver(mutations => { + for (const mutation of mutations) { + tNodeList(mutation.addedNodes); + } + }); + const onLoad = () => { + tDocLoader.stop(); + tNodeList(document.all); + }; + tDocLoader.start = () => { + observer.observe(document, {subtree: true, childList: true}); + }; + tDocLoader.stop = () => { + observer.disconnect(); + document.removeEventListener('DOMContentLoaded', onLoad); + }; + tDocLoader.start(); + document.addEventListener('DOMContentLoaded', onLoad); } diff --git a/manage.html b/manage.html index 9f5fc519..023f4532 100644 --- a/manage.html +++ b/manage.html @@ -125,7 +125,6 @@ - diff --git a/manage.js b/manage.js index 5fbe0e72..24c62757 100644 --- a/manage.js +++ b/manage.js @@ -1,4 +1,5 @@ -/* globals styleSectionsEqual */ +/* global messageBox */ +'use strict'; const installed = $('#installed'); const TARGET_LABEL = t('appliesDisplay', '').trim(); @@ -11,7 +12,7 @@ getStylesSafe({code: false}) .then(initGlobalEvents); -chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { +chrome.runtime.onMessage.addListener(msg => { switch (msg.method) { case 'styleUpdated': case 'styleAdded': @@ -45,7 +46,7 @@ function initGlobalEvents() { }; // remember scroll position on normal history navigation - document.addEventListener('visibilitychange', event => { + document.addEventListener('visibilitychange', () => { if (document.visibilityState != 'visible') { rememberScrollPosition(); } @@ -72,7 +73,7 @@ function initGlobalEvents() { function showStyles(styles = []) { const sorted = styles .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 renderBin = document.createDocumentFragment(); 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 function renderStyles(index) { const t0 = performance.now(); - while (index < sorted.length && (shouldRenderAll || performance.now() - t0 < 10)) { + while (index < sorted.length) { renderBin.appendChild(createStyleElement(sorted[index++].style)); + if (!shouldRenderAll && performance.now() - t0 > 10) { + break; + } } if ($('#search').value) { // re-apply filtering on history Back - searchStyles(true, renderBin); + searchStyles({immediately: true, container: renderBin}); } installed.appendChild(renderBin); if (index < sorted.length) { 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)); } } } +// silence the inapplicable warning for async code +/* eslint no-use-before-define: [2, {"functions": false, "classes": false}] */ function createStyleElement(style) { const entry = template.style.cloneNode(true); entry.classList.add(style.enabled ? 'enabled' : 'disabled'); @@ -131,9 +136,9 @@ function createStyleElement(style) { regexpsBefore: '/', regexpsAfter: '/', }; - for (let [name, target] of targets.entries()) { - for (let section of style.sections) { - for (let targetValue of section[name] || []) { + for (const [name, target] of targets.entries()) { + for (const section of style.sections) { + for (const targetValue of section[name] || []) { target.add( (decorations[name + 'Before'] || '') + targetValue.trim() + @@ -151,7 +156,7 @@ function createStyleElement(style) { } else { let index = 0; let container = appliesTo; - for (let target of targetsList) { + for (const target of targetsList) { if (index > 0) { container.appendChild(template.appliesToSeparator.cloneNode(true)); } @@ -184,8 +189,10 @@ class EntryOnClick { } event.preventDefault(); event.stopPropagation(); - const left = event.button == 0, middle = event.button == 1, - shift = event.shiftKey, ctrl = event.ctrlKey; + const left = event.button == 0; + const middle = event.button == 1; + const shift = event.shiftKey; + const ctrl = event.ctrlKey; const openWindow = left && shift && !ctrl; const openBackgroundTab = (middle && !shift) || (left && ctrl && !shift); const openForegroundTab = (middle && shift) || (left && ctrl && shift); @@ -215,10 +222,10 @@ class EntryOnClick { } static update(event) { - const updatedCode = getClickedStyleElement(event).updatedCode; + const styleElement = getClickedStyleElement(event); // update everything but name - saveStyle(Object.assign(updatedCode, { - id: element.styleId, + saveStyle(Object.assign(styleElement.updatedCode, { + id: styleElement.styleId, name: null, reason: 'update', })); @@ -340,10 +347,10 @@ class Updater { } checkMd5() { - return this.download(this.md5Url).then( - md5 => md5.length == 32 + return Updater.download(this.md5Url).then( + md5 => (md5.length == 32 ? this.decideOnMd5(md5 != this.md5) - : this.onFailure(-1), + : this.onFailure(-1)), this.onFailure); } @@ -355,7 +362,7 @@ class Updater { } checkFullCode({forceUpdate = false} = {}) { - return this.download(this.url).then( + return Updater.download(this.url).then( text => this.handleJson(forceUpdate, JSON.parse(text)), this.onFailure); } @@ -391,12 +398,12 @@ class Updater { } } - download(url) { + static download(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); - xhr.onloadend = () => xhr.status == 200 + xhr.onloadend = () => (xhr.status == 200 ? resolve(xhr.responseText) - : reject(xhr.status); + : reject(xhr.status)); if (url.length > 2000) { const [mainUrl, query] = url.split('?'); xhr.open('POST', mainUrl, true); @@ -412,19 +419,19 @@ class Updater { } -function searchStyles(immediately, bin) { +function searchStyles({immediately, container}) { const query = $('#search').value.toLocaleLowerCase(); - if (query == (searchStyles.lastQuery || '') && !bin) { + if (query == (searchStyles.lastQuery || '') && !container) { return; } searchStyles.lastQuery = query; if (!immediately) { clearTimeout(searchStyles.timeout); - searchStyles.timeout = setTimeout(doSearch, 200, true); + searchStyles.timeout = setTimeout(searchStyles, 200, {immediately: true}); return; } - for (let element of (bin || installed).children) { + for (const element of (container || installed).children) { const {style} = cachedStyles.byId.get(element.styleId) || {}; if (style) { const isMatching = !query || isMatchingText(style.name) || isMatchingStyle(style); @@ -433,8 +440,8 @@ function searchStyles(immediately, bin) { } function isMatchingStyle(style) { - for (let section of style.sections) { - for (let prop in section) { + for (const section of style.sections) { + for (const prop in section) { const value = section[prop]; switch (typeof value) { case 'string': @@ -443,7 +450,7 @@ function searchStyles(immediately, bin) { } break; case 'object': - for (let str of value) { + for (const str of value) { if (isMatchingText(str)) { return true; } diff --git a/messaging.js b/messaging.js index 4296f4cf..6bb697e6 100644 --- a/messaging.js +++ b/messaging.js @@ -1,187 +1,190 @@ +/* global getStyleWithNoCode, applyOnMessage, onBackgroundMessage, getStyles */ +'use strict'; + // keep message channel open for sendResponse in chrome.runtime.onMessage listener const KEEP_CHANNEL_OPEN = true; const OWN_ORIGIN = chrome.runtime.getURL(''); function notifyAllTabs(request) { - // list all tabs including chrome-extension:// which can be ours - if (request.codeIsUpdated === false && request.style) { - request = Object.assign({}, request, { - style: getStyleWithNoCode(request.style) + // list all tabs including chrome-extension:// which can be ours + if (request.codeIsUpdated === false && request.style) { + request = Object.assign({}, request, { + style: getStyleWithNoCode(request.style) }); - } - chrome.tabs.query({}, tabs => { - for (let tab of tabs) { - chrome.tabs.sendMessage(tab.id, request); - updateIcon(tab); - } - }); - // notify all open popups - const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method}); - chrome.runtime.sendMessage(reqPopup); - // notify self: the message no longer is sent to the origin in new Chrome - if (typeof applyOnMessage !== 'undefined') { - applyOnMessage(reqPopup); - } - // notify self: pref changed by background page - if (request.method == 'prefChanged' && typeof onBackgroundMessage !== 'undefined') { - onBackgroundMessage(request); - } + } + chrome.tabs.query({}, tabs => { + for (const tab of tabs) { + chrome.tabs.sendMessage(tab.id, request); + updateIcon(tab); + } + }); + // notify all open popups + const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method}); + chrome.runtime.sendMessage(reqPopup); + // notify self: the message no longer is sent to the origin in new Chrome + if (typeof applyOnMessage !== 'undefined') { + applyOnMessage(reqPopup); + } + // notify self: pref changed by background page + if (request.method == 'prefChanged' && typeof onBackgroundMessage !== 'undefined') { + onBackgroundMessage(request); + } } function refreshAllTabs() { - return new Promise(resolve => { - // list all tabs including chrome-extension:// which can be ours - chrome.tabs.query({}, tabs => { - const lastTab = tabs[tabs.length - 1]; - for (let tab of tabs) { - getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => { - const message = {method: 'styleReplaceAll', styles}; - if (tab.url == location.href && typeof applyOnMessage !== 'undefined') { - applyOnMessage(message); - } else { - chrome.tabs.sendMessage(tab.id, message); - } - updateIcon(tab, styles); - if (tab == lastTab) { - resolve(); - } - }); - } - }); - }); + return new Promise(resolve => { + // list all tabs including chrome-extension:// which can be ours + chrome.tabs.query({}, tabs => { + const lastTab = tabs[tabs.length - 1]; + for (const tab of tabs) { + getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => { + const message = {method: 'styleReplaceAll', styles}; + if (tab.url == location.href && typeof applyOnMessage !== 'undefined') { + applyOnMessage(message); + } else { + chrome.tabs.sendMessage(tab.id, message); + } + updateIcon(tab, styles); + if (tab == lastTab) { + resolve(); + } + }); + } + }); + }); } function updateIcon(tab, styles) { - // while NTP is still loading only process the request for its main frame with a real url - // (but when it's loaded we should process style toggle requests from popups, for example) - if (tab.url == 'chrome://newtab/' && tab.status != 'complete') { - return; - } - if (styles) { - // check for not-yet-existing tabs e.g. omnibox instant search - chrome.tabs.get(tab.id, () => { - if (!chrome.runtime.lastError) { - stylesReceived(styles); - } - }); - return; - } - getTabRealURL(tab).then(url => { - // if we have access to this, call directly - // (Chrome no longer sends messages to the page itself) - const options = {method: 'getStyles', matchUrl: url, enabled: true, asHash: true}; - if (typeof getStyles != 'undefined') { - getStyles(options, stylesReceived); - } else { - chrome.runtime.sendMessage(options, stylesReceived); - } - }); + // while NTP is still loading only process the request for its main frame with a real url + // (but when it's loaded we should process style toggle requests from popups, for example) + if (tab.url == 'chrome://newtab/' && tab.status != 'complete') { + return; + } + if (styles) { + // check for not-yet-existing tabs e.g. omnibox instant search + chrome.tabs.get(tab.id, () => { + if (!chrome.runtime.lastError) { + stylesReceived(styles); + } + }); + return; + } + getTabRealURL(tab).then(url => { + // if we have access to this, call directly + // (Chrome no longer sends messages to the page itself) + const options = {method: 'getStyles', matchUrl: url, enabled: true, asHash: true}; + if (typeof getStyles != 'undefined') { + getStyles(options, stylesReceived); + } else { + chrome.runtime.sendMessage(options, stylesReceived); + } + }); - function stylesReceived(styles) { - let numStyles = styles.length; - if (numStyles === undefined) { - // for 'styles' asHash:true fake the length by counting numeric ids manually - numStyles = 0; - for (let id of Object.keys(styles)) { - numStyles += id.match(/^\d+$/) ? 1 : 0; - } - } - const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); - const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : ''; - chrome.browserAction.setIcon({ - path: { - // Material Design 2016 new size is 16px - 16: `/images/icon/16${postfix}.png`, 32: `/images/icon/32${postfix}.png`, - // Chromium forks or non-chromium browsers may still use the traditional 19px - 19: `/images/icon/19${postfix}.png`, 38: `/images/icon/38${postfix}.png`, - }, - tabId: tab.id - }, () => { - // if the tab was just closed an error may occur, - // e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload - if (!chrome.runtime.lastError) { - const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; - chrome.browserAction.setBadgeText({text, tabId: tab.id}); - chrome.browserAction.setBadgeBackgroundColor({ - color: prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal') - }); - } - }); - } + function stylesReceived(styles) { + let numStyles = styles.length; + if (numStyles === undefined) { + // for 'styles' asHash:true fake the length by counting numeric ids manually + numStyles = 0; + for (const id of Object.keys(styles)) { + numStyles += id.match(/^\d+$/) ? 1 : 0; + } + } + const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); + const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : ''; + chrome.browserAction.setIcon({ + path: { + // Material Design 2016 new size is 16px + 16: `/images/icon/16${postfix}.png`, 32: `/images/icon/32${postfix}.png`, + // Chromium forks or non-chromium browsers may still use the traditional 19px + 19: `/images/icon/19${postfix}.png`, 38: `/images/icon/38${postfix}.png`, + }, + tabId: tab.id + }, () => { + // if the tab was just closed an error may occur, + // e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload + if (!chrome.runtime.lastError) { + const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; + chrome.browserAction.setBadgeText({text, tabId: tab.id}); + chrome.browserAction.setBadgeBackgroundColor({ + color: prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal') + }); + } + }); + } } function getActiveTab() { - return new Promise(resolve => - chrome.tabs.query({currentWindow: true, active: true}, tabs => - resolve(tabs[0]))); + return new Promise(resolve => + chrome.tabs.query({currentWindow: true, active: true}, tabs => + resolve(tabs[0]))); } function getActiveTabRealURL() { - return getActiveTab() - .then(getTabRealURL); + return getActiveTab() + .then(getTabRealURL); } function getTabRealURL(tab) { - return new Promise(resolve => { - if (tab.url != 'chrome://newtab/') { - resolve(tab.url); - } else { - chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => { - frame && resolve(frame.url); - }); - } - }); + return new Promise(resolve => { + if (tab.url != 'chrome://newtab/') { + resolve(tab.url); + } else { + chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => { + frame && resolve(frame.url); + }); + } + }); } // opens a tab or activates the already opened one, // reuses the New Tab page if it's focused now function openURL({url, currentWindow = true}) { - if (!url.includes('://')) { - url = chrome.runtime.getURL(url); - } - return new Promise(resolve => { - chrome.tabs.query({url, currentWindow}, tabs => { - if (tabs.length) { - activateTab(tabs[0]).then(resolve); - } else { - getActiveTab().then(tab => - tab && tab.url == 'chrome://newtab/' - ? chrome.tabs.update({url}, resolve) - : chrome.tabs.create({url}, resolve) - ); - } - }); - }); + if (!url.includes('://')) { + url = chrome.runtime.getURL(url); + } + return new Promise(resolve => { + chrome.tabs.query({url, currentWindow}, tabs => { + if (tabs.length) { + activateTab(tabs[0]).then(resolve); + } else { + getActiveTab().then(tab => ( + tab && tab.url == 'chrome://newtab/' + ? chrome.tabs.update({url}, resolve) + : chrome.tabs.create({url}, resolve) + )); + } + }); + }); } function activateTab(tab) { - return Promise.all([ - new Promise(resolve => { - chrome.tabs.update(tab.id, {active: true}, resolve); - }), - new Promise(resolve => { - chrome.windows.update(tab.windowId, {focused: true}, resolve); - }), - ]); + return Promise.all([ + new Promise(resolve => { + chrome.tabs.update(tab.id, {active: true}, resolve); + }), + new Promise(resolve => { + chrome.windows.update(tab.windowId, {focused: true}, resolve); + }), + ]); } function stringAsRegExp(s, flags) { - return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, '\\$&'), flags); + return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags); } // expands * as .*? function wildcardAsRegExp(s, flags) { - return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags); + return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&').replace(/\*/g, '.*?'), flags); } @@ -189,14 +192,14 @@ function wildcardAsRegExp(s, flags) { // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers // * Functions that contain object literals that contain __proto__, or get or set declarations. const configureCommands = (() => ({ - get url () { - return navigator.userAgent.indexOf('OPR') > -1 ? - 'opera://settings/configureCommands' : - 'chrome://extensions/configureCommands' - }, - open: () => { - chrome.tabs.create({ - 'url': configureCommands.url - }); - } + get url() { + return navigator.userAgent.includes('OPR') + ? 'opera://settings/configureCommands' + : 'chrome://extensions/configureCommands'; + }, + open: () => { + chrome.tabs.create({ + 'url': configureCommands.url + }); + } }))(); diff --git a/options/index.js b/options/index.js index 6808d933..c2681fa3 100644 --- a/options/index.js +++ b/options/index.js @@ -1,8 +1,7 @@ -/* globals configureCommands */ 'use strict'; -function restore () { +function restore() { chrome.runtime.getBackgroundPage(bg => { $('#badgeDisabled').value = bg.prefs.get('badgeDisabled'); $('#badgeNormal').value = bg.prefs.get('badgeNormal'); @@ -13,28 +12,28 @@ function restore () { } -function save () { +function save() { chrome.runtime.getBackgroundPage(bg => { bg.prefs.set('badgeDisabled', $('#badgeDisabled').value); bg.prefs.set('badgeNormal', $('#badgeNormal').value); localStorage.setItem('popupWidth', enforceValueRange('popupWidth')); bg.prefs.set( 'updateInterval', - Math.max(0, +$('#updateInterval').value) + Math.max(0, Number($('#updateInterval').value)) ); // display notification - let status = $('#status'); + const status = $('#status'); status.textContent = 'Options saved.'; - setTimeout(() => status.textContent = '', 750); + setTimeout(() => (status.textContent = ''), 750); }); } function enforceValueRange(id) { - let element = document.getElementById(id); - let value = Number(element.value); + const element = document.getElementById(id); const min = Number(element.min); const max = Number(element.max); + let value = Number(element.value); if (value < min || value > max) { value = Math.max(min, Math.min(max, value)); element.value = value; @@ -53,40 +52,38 @@ $('[data-cmd="open-keyboard"]').textContent = // actions document.onclick = e => { - let cmd = e.target.dataset.cmd; - let total = 0, updated = 0; + const cmd = e.target.dataset.cmd; + let total = 0; + let updated = 0; - function update () { + function update() { $('#update-counter').textContent = `${updated}/${total}`; } - function done (target) { + + function done(target) { target.disabled = false; window.setTimeout(() => { $('#update-counter').textContent = ''; }, 750); } - switch (cmd) { - - case 'open-manage': - openURL({url: '/manage.html'}); - break; - - case'check-updates': - e.target.disabled = true; + function check() { chrome.runtime.getBackgroundPage(bg => { bg.update.perform((cmd, value) => { - if (cmd === 'count') { - total = value; - if (!total) { - done(e.target); - } - } - else if (cmd === 'single-updated' || cmd === 'single-skipped') { - updated += 1; - if (total && updated === total) { - done(e.target); - } + switch (cmd) { + case 'count': + total = value; + if (!total) { + done(e.target); + } + break; + case 'single-updated': + case 'single-skipped': + updated++; + if (total && updated === total) { + done(e.target); + } + break; } update(); }); @@ -95,11 +92,21 @@ document.onclick = e => { chrome.runtime.sendMessage({ method: 'resetInterval' }); - break; + } - case 'open-keyboard': - configureCommands.open(); - break; + switch (cmd) { + case 'open-manage': + openURL({url: '/manage.html'}); + break; + + case 'check-updates': + e.target.disabled = true; + check(); + break; + + case 'open-keyboard': + configureCommands.open(); + break; } }; diff --git a/popup.js b/popup.js index 76ed2579..9c541e7b 100644 --- a/popup.js +++ b/popup.js @@ -1,11 +1,9 @@ -/* globals configureCommands, openURL */ +'use strict'; -const RX_SUPPORTED_URLS = new RegExp( - `^(file|https?|ftps?):|^${OWN_ORIGIN}`); let installed; - getActiveTabRealURL().then(url => { + const RX_SUPPORTED_URLS = new RegExp(`^(file|https?|ftps?):|^${OWN_ORIGIN}`); const isUrlSupported = RX_SUPPORTED_URLS.test(url); Promise.all([ 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') { switch (msg.reason) { case 'styleAdded': @@ -93,7 +91,7 @@ function initPopup(url) { // For domain const domains = getDomains(url); - for (let domain of domains) { + for (const domain of domains) { // Don't include TLD if (domains.length > 1 && !domain.includes('.')) { continue; @@ -122,12 +120,13 @@ function showStyles(styles) { installed.innerHTML = template.noStyles.outerHTML; } else { const enabledFirst = prefs.get('popup.enabledFirst'); - styles.sort((a, b) => + styles.sort((a, b) => ( enabledFirst && a.enabled !== b.enabled ? !(a.enabled < b.enabled) ? -1 : 1 - : a.name.localeCompare(b.name)); + : a.name.localeCompare(b.name) + )); const fragment = document.createDocumentFragment(); - for (let style of styles) { + for (const style of styles) { fragment.appendChild(createStyleElement(style)); } 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) { const entry = template.style.cloneNode(true); entry.setAttribute('style-id', style.id); @@ -206,7 +207,7 @@ class EntryOnClick { function confirm(ok) { window.onkeydown = null; animateElement(box, {className: 'lights-on'}) - .then(() => box.dataset.display = false); + .then(() => (box.dataset.display = false)); if (ok) { deleteStyle(id).then(() => { // update view with 'No styles installed for this site' message @@ -279,7 +280,7 @@ function handleUpdate(style) { function handleDelete(id) { - var styleElement = $(`[style-id="${id}"]`, installed); + const styleElement = $(`[style-id="${id}"]`, installed); if (styleElement) { installed.removeChild(styleElement); } diff --git a/storage-websql.js b/storage-websql.js index 16c8823c..2ebb09a8 100644 --- a/storage-websql.js +++ b/storage-websql.js @@ -1,169 +1,241 @@ -var webSqlStorage = { +/* global getDatabase, reportError */ +'use strict'; - migrate: function() { - if (typeof openDatabase == "undefined") { - // No WebSQL - no migration! - return; - } - webSqlStorage.getStyles(function(styles) { - getDatabase(function(db) { - var tx = db.transaction(["styles"], "readwrite"); - var os = tx.objectStore("styles"); - styles.forEach(function(s) { - webSqlStorage.cleanStyle(s) - os.add(s); - }); - // While this was running, the styles were loaded from the (empty) indexed db - setTimeout(function() { - invalidateCache(true); - }, 500); - }); - }, null); - }, +const webSqlStorage = { - cleanStyle: function(s) { - delete s.id; - s.sections.forEach(function(section) { - delete section.id; - ["urls", "urlPrefixes", "domains", "regexps"].forEach(function(property) { - if (!section[property]) { - section[property] = []; - } - }); - }); - }, + migrate() { + if (typeof openDatabase == 'undefined') { + // No WebSQL - no migration! + return; + } + webSqlStorage.getStyles(styles => { + getDatabase(db => { + const tx = db.transaction(['styles'], 'readwrite'); + const os = tx.objectStore('styles'); + styles.forEach(s => { + webSqlStorage.cleanStyle(s); + os.add(s); + }); + // While this was running, the styles were loaded from the (empty) indexed db + setTimeout(() => invalidateCache(true), 500); + }); + }, null); + }, - getStyles: function(callback) { - webSqlStorage.getDatabase(function(db) { - if (!db) { - callback([]); - return; - } - db.readTransaction(function (t) { - var where = ""; - var params = []; + cleanStyle(s) { + delete s.id; + s.sections.forEach(section => { + delete section.id; + ['urls', 'urlPrefixes', 'domains', 'regexps'].forEach(property => { + if (!section[property]) { + section[property] = []; + } + }); + }); + }, - 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) { - var styles = []; - var currentStyle = null; - var currentSection = null; - for (var i = 0; i < r.rows.length; i++) { - var values = r.rows.item(i); - var metaName = null; - switch (values.metaName) { - case null: - break; - case "url": - metaName = "urls"; - break; - case "url-prefix": - metaName = "urlPrefixes"; - break; - case "domain": - var metaName = "domains"; - break; - case "regexps": - var metaName = "regexps"; - break; - default: - var metaName = values.metaName + "s"; - } - var metaValue = values.metaValue; - 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: []}; - styles.push(currentStyle); - } - if (values.section_id != null) { - if (currentSection == null || currentSection.id != values.section_id) { - currentSection = {id: values.section_id, code: values.code}; - currentStyle.sections.push(currentSection); - } - if (metaName && metaValue) { - if (currentSection[metaName]) { - currentSection[metaName].push(metaValue); - } else { - currentSection[metaName] = [metaValue]; - } - } - } - } - callback(styles); - }, reportError); - }, reportError); - }, reportError); - }, + getStyles(callback) { + webSqlStorage.getDatabase(db => { + if (!db) { + callback([]); + return; + } + db.readTransaction(t => { + const where = ''; + const params = []; - getDatabase: function(ready, error) { - try { - stylishDb = openDatabase('stylish', '', 'Stylish Styles', 5*1024*1024); - } catch (ex) { - error(); - throw ex; - } - if (stylishDb.version == "") { - // It didn't already exist, we have nothing to migrate. - ready(null); - return; - } - if (stylishDb.version == "1.0") { - webSqlStorage.dbV11(stylishDb, error, ready); - } else if (stylishDb.version == "1.1") { - webSqlStorage.dbV12(stylishDb, error, ready); - } else if (stylishDb.version == "1.2") { - webSqlStorage.dbV13(stylishDb, error, ready); - } else if (stylishDb.version == "1.3") { - webSqlStorage.dbV14(stylishDb, error, ready); - } else if (stylishDb.version == "1.4") { - webSqlStorage.dbV15(stylishDb, error, ready); - } else { - ready(stylishDb); - } - }, + 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, + (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) { + case null: + break; + case 'url': + metaName = 'urls'; + break; + case 'url-prefix': + metaName = 'urlPrefixes'; + break; + case 'domain': + metaName = 'domains'; + break; + case 'regexps': + metaName = 'regexps'; + break; + default: + metaName = values.metaName + 's'; + } + const metaValue = values.metaValue; + 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: [] + }; + styles.push(currentStyle); + } + if (values.section_id !== null) { + if (currentSection === null || currentSection.id != values.section_id) { + currentSection = {id: values.section_id, code: values.code}; + currentStyle.sections.push(currentSection); + } + if (metaName && metaValue) { + if (currentSection[metaName]) { + currentSection[metaName].push(metaValue); + } else { + currentSection[metaName] = [metaValue]; + } + } + } + } + callback(styles); + }, reportError); + }, reportError); + }, reportError); + }, - dbV11: function(d, error, done) { - d.changeVersion(d.version, '1.1', function (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('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);'); - }, error, function() { webSqlStorage.dbV12(d, error, done)}); - }, + getDatabase(ready, error) { + let stylishDb; + try { + stylishDb = openDatabase('stylish', '', 'Stylish Styles', 5 * 1024 * 1024); + } catch (ex) { + error(); + throw ex; + } + if (stylishDb.version == '') { + // It didn't already exist, we have nothing to migrate. + ready(null); + return; + } + switch (stylishDb.version) { + case '1.0': return webSqlStorage.dbV11(stylishDb, error, ready); + case '1.1': return webSqlStorage.dbV12(stylishDb, error, ready); + case '1.2': return webSqlStorage.dbV13(stylishDb, error, ready); + case '1.3': return webSqlStorage.dbV14(stylishDb, error, ready); + case '1.4': return webSqlStorage.dbV15(stylishDb, error, ready); + default: ready(stylishDb); + } + }, - dbV12: function(d, error, done) { - d.changeVersion(d.version, '1.2', function (t) { - // 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('INSERT INTO sections (style_id, code) SELECT id, code FROM styles;'); - // switch meta to sections - t.executeSql('DROP INDEX style_meta_style_id;'); - 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);'); - 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 - 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('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, function() { webSqlStorage.dbV13(d, error, done)}); - }, + dbV11(d, error, done) { + 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( + '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);'); + }, error, () => webSqlStorage.dbV12(d, error, done)); + }, - dbV13: function(d, error, done) { - d.changeVersion(d.version, '1.3', function (t) { - // 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('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, function() { webSqlStorage.dbV14(d, error, done)}); - }, + dbV12(d, error, done) { + d.changeVersion(d.version, '1.2', t => { + // 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( + 'INSERT INTO sections (style_id, code) SELECT id, code FROM styles;'); + // switch meta to sections + t.executeSql( + 'DROP INDEX style_meta_style_id;'); + 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);'); + 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 + 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( + '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)); + }, - dbV14: function(d, error, done) { - d.changeVersion(d.version, '1.4', function (t) { - t.executeSql('UPDATE styles SET url = null WHERE url = "undefined";'); - }, error, function() { webSqlStorage.dbV15(d, error, done)}); - }, + dbV13(d, error, done) { + d.changeVersion(d.version, '1.3', t => { + // 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( + '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)); + }, - dbV15: function(d, error, done) { - d.changeVersion(d.version, '1.5', function (t) { - t.executeSql('ALTER TABLE styles ADD COLUMN originalMd5 TEXT NULL;'); - }, error, function() { done(d); }); - } -} + dbV14(d, error, done) { + d.changeVersion(d.version, '1.4', t => { + t.executeSql( + 'UPDATE styles SET url = null ' + + 'WHERE url = "undefined";'); + }, error, () => webSqlStorage.dbV15(d, error, done)); + }, + + dbV15(d, error, done) { + d.changeVersion(d.version, '1.5', t => { + t.executeSql( + 'ALTER TABLE styles ' + + 'ADD COLUMN originalMd5 TEXT NULL;'); + }, error, () => done(d)); + } +}; diff --git a/storage.js b/storage.js index 6a492f58..3ea3efc7 100644 --- a/storage.js +++ b/storage.js @@ -1,834 +1,849 @@ +/* global cachedStyles: true, prefs: true, contextMenus: false */ +/* global handleUpdate, handleDelete */ +/* global webSqlStorage */ +'use strict'; + function getDatabase(ready, error) { - var dbOpenRequest = window.indexedDB.open("stylish", 2); - dbOpenRequest.onsuccess = function(e) { - ready(e.target.result); - }; - dbOpenRequest.onerror = function(event) { - console.log(event.target.errorCode); - if (error) { - error(event); - } - }; - dbOpenRequest.onupgradeneeded = function(event) { - if (event.oldVersion == 0) { - var os = event.target.result.createObjectStore("styles", {keyPath: 'id', autoIncrement: true}); - webSqlStorage.migrate(); - } - } -}; + const dbOpenRequest = window.indexedDB.open('stylish', 2); + dbOpenRequest.onsuccess = event => { + ready(event.target.result); + }; + dbOpenRequest.onerror = event => { + console.warn(event.target.errorCode); + if (error) { + error(event); + } + }; + dbOpenRequest.onupgradeneeded = event => { + if (event.oldVersion == 0) { + event.target.result.createObjectStore('styles', { + keyPath: 'id', + autoIncrement: true, + }); + webSqlStorage.migrate(); + } + }; +} // Let manage/popup/edit reuse background page variables -// Note, only "var"-declared variables are visible from another extension page -var cachedStyles = ((bg) => bg && bg.cachedStyles || { - bg, - list: null, - noCode: null, - byId: new Map(), - filters: new Map(), - mutex: { - inProgress: false, - onDone: [], - }, -})(chrome.extension.getBackgroundPage()); +// Note, only 'var'-declared variables are visible from another extension page +// eslint-disable-next-line no-var +var cachedStyles, prefs; +(() => { + const bg = chrome.extension.getBackgroundPage(); + cachedStyles = bg && bg.cachedStyles || { + bg, + list: null, + noCode: null, + byId: new Map(), + filters: new Map(), + mutex: { + inProgress: false, + onDone: [], + }, + }; + prefs = bg && bg.prefs; +})(); // in case Chrome haven't yet loaded the bg page and displays our page like edit/manage function getStylesSafe(options) { - return new Promise(resolve => { - if (cachedStyles.bg) { - getStyles(options, resolve); - return; - } - chrome.runtime.sendMessage(Object.assign({method: 'getStyles'}, options), styles => { - if (!styles) { - resolve(getStylesSafe(options)); - } else { - cachedStyles = chrome.extension.getBackgroundPage().cachedStyles; - resolve(styles); - } - }); - }); + return new Promise(resolve => { + if (cachedStyles.bg) { + getStyles(options, resolve); + return; + } + chrome.runtime.sendMessage(Object.assign({method: 'getStyles'}, options), styles => { + if (!styles) { + resolve(getStylesSafe(options)); + } else { + cachedStyles = chrome.extension.getBackgroundPage().cachedStyles; + resolve(styles); + } + }); + }); } function getStyles(options, callback) { - if (cachedStyles.list) { - callback(filterStyles(options)); - return; - } - if (cachedStyles.mutex.inProgress) { - cachedStyles.mutex.onDone.push({options, callback}); - return; - } - cachedStyles.mutex.inProgress = true; + if (cachedStyles.list) { + callback(filterStyles(options)); + return; + } + if (cachedStyles.mutex.inProgress) { + cachedStyles.mutex.onDone.push({options, callback}); + return; + } + cachedStyles.mutex.inProgress = true; - const t0 = performance.now() - getDatabase(db => { - const tx = db.transaction(['styles'], 'readonly'); - const os = tx.objectStore('styles'); - os.getAll().onsuccess = event => { - cachedStyles.list = event.target.result || []; - cachedStyles.noCode = []; - cachedStyles.byId.clear(); - for (let style of cachedStyles.list) { - const noCode = getStyleWithNoCode(style); - cachedStyles.noCode.push(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))) - try{ - callback(filterStyles(options)); - } catch(e){ - // no error in console, it works - } + //const t0 = performance.now(); + getDatabase(db => { + const tx = db.transaction(['styles'], 'readonly'); + const os = tx.objectStore('styles'); + os.getAll().onsuccess = event => { + cachedStyles.list = event.target.result || []; + cachedStyles.noCode = []; + cachedStyles.byId.clear(); + for (const style of cachedStyles.list) { + const noCode = getStyleWithNoCode(style); + cachedStyles.noCode.push(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))) + runTryCatch(callback, filterStyles(options)); - cachedStyles.mutex.inProgress = false; - for (let {options, callback} of cachedStyles.mutex.onDone) { - callback(filterStyles(options)); - } - cachedStyles.mutex.onDone = []; - }; - }, null); + cachedStyles.mutex.inProgress = false; + for (const {options, callback} of cachedStyles.mutex.onDone) { + callback(filterStyles(options)); + } + cachedStyles.mutex.onDone = []; + }; + }, null); } function getStyleWithNoCode(style) { - const stripped = Object.assign({}, style, {sections: []}); - for (let section of style.sections) { - stripped.sections.push(Object.assign({}, section, {code: null})); - } - return stripped; + const stripped = Object.assign({}, style, {sections: []}); + for (const section of style.sections) { + stripped.sections.push(Object.assign({}, section, {code: null})); + } + return stripped; } function invalidateCache(andNotify, {added, updated, deletedId} = {}) { - // prevent double-add on echoed invalidation - const cached = added && cachedStyles.byId.get(added.id); - if (cached) { - return; - } - if (andNotify) { - chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId}); - } - if (!cachedStyles.list) { - return; - } - if (updated) { - const cached = cachedStyles.byId.get(updated.id); - if (cached) { - Object.assign(cached.style, updated); - Object.assign(cached.noCode, getStyleWithNoCode(updated)); - //console.log('cache: updated', updated); - } - cachedStyles.filters.clear(); - return; - } - if (added) { - const noCode = getStyleWithNoCode(added); - cachedStyles.list.push(added); - cachedStyles.noCode.push(noCode); - cachedStyles.byId.set(added.id, {style: added, noCode}); - //console.log('cache: added', added); - cachedStyles.filters.clear(); - return; - } - if (deletedId != undefined) { - const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style; - if (deletedStyle) { - const cachedIndex = cachedStyles.list.indexOf(deletedStyle); - cachedStyles.list.splice(cachedIndex, 1); - cachedStyles.noCode.splice(cachedIndex, 1); - cachedStyles.byId.delete(deletedId); - //console.log('cache: deleted', deletedStyle); - cachedStyles.filters.clear(); - return; - } - } - cachedStyles.list = null; - cachedStyles.noCode = null; - //console.log('cache cleared'); - cachedStyles.filters.clear(); + // prevent double-add on echoed invalidation + const cached = added && cachedStyles.byId.get(added.id); + if (cached) { + return; + } + if (andNotify) { + chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId}); + } + if (!cachedStyles.list) { + return; + } + if (updated) { + const cached = cachedStyles.byId.get(updated.id); + if (cached) { + Object.assign(cached.style, updated); + Object.assign(cached.noCode, getStyleWithNoCode(updated)); + //console.log('cache: updated', updated); + } + cachedStyles.filters.clear(); + return; + } + if (added) { + const noCode = getStyleWithNoCode(added); + cachedStyles.list.push(added); + cachedStyles.noCode.push(noCode); + cachedStyles.byId.set(added.id, {style: added, noCode}); + //console.log('cache: added', added); + cachedStyles.filters.clear(); + return; + } + if (deletedId != undefined) { + const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style; + if (deletedStyle) { + const cachedIndex = cachedStyles.list.indexOf(deletedStyle); + cachedStyles.list.splice(cachedIndex, 1); + cachedStyles.noCode.splice(cachedIndex, 1); + cachedStyles.byId.delete(deletedId); + //console.log('cache: deleted', deletedStyle); + cachedStyles.filters.clear(); + return; + } + } + cachedStyles.list = null; + cachedStyles.noCode = null; + //console.log('cache cleared'); + cachedStyles.filters.clear(); } function filterStyles(options = {}) { - const t0 = performance.now() - const enabled = fixBoolean(options.enabled); - const url = 'url' in options ? options.url : null; - const id = 'id' in options ? Number(options.id) : null; - const matchUrl = 'matchUrl' in options ? options.matchUrl : null; - const code = 'code' in options ? options.code : true; - const asHash = 'asHash' in options ? options.asHash : false; + //const t0 = performance.now(); + const enabled = fixBoolean(options.enabled); + const url = 'url' in options ? options.url : null; + const id = 'id' in options ? Number(options.id) : null; + const matchUrl = 'matchUrl' in options ? options.matchUrl : null; + const code = 'code' in options ? options.code : true; + const asHash = 'asHash' in options ? options.asHash : false; - if (enabled == null - && url == null - && id == null - && matchUrl == null - && asHash != true) { - //console.log('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) - return code ? cachedStyles.list : cachedStyles.noCode; - } + if (enabled === null + && url === null + && id === null + && matchUrl === null + && asHash != true) { + //console.log('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) + 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) - const cacheKey = '' + enabled + url + '\t' + id + matchUrl + '\t' + code + asHash; - const cached = cachedStyles.filters.get(cacheKey); - if (cached) { - //console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) - cached.hits++; - cached.lastHit = Date.now(); - return asHash - ? Object.assign({disableAll: prefs.get('disableAll', false)}, cached.styles) - : cached.styles; - } + // 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 cached = cachedStyles.filters.get(cacheKey); + if (cached) { + //console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options)) + cached.hits++; + cached.lastHit = Date.now(); - const styles = id == null - ? (code ? cachedStyles.list : cachedStyles.noCode) - : [(cachedStyles.byId.get(id) || {})[code ? 'style' : 'noCode']]; - const filtered = asHash ? {} : []; - if (!styles) { - // may happen when users [accidentally] reopen an old URL - // of edit.html with a non-existent style id parameter - return filtered; - } - for (let i = 0, style; (style = styles[i]); i++) { - if ((enabled == null || style.enabled == enabled) - && (url == null || style.url == url) - && (id == null || style.id == id)) { - const sections = (asHash || matchUrl != null) && getApplicableSections(style, matchUrl); - if (asHash) { - if (sections.length) { - filtered[style.id] = sections; - } - } else if (matchUrl == null || sections.length) { - filtered.push(style); - } - } - } - //console.log('%s filterStyles %s', (performance.now() - t0).toFixed(1), JSON.stringify(options)) - cachedStyles.filters.set(cacheKey, { - styles: filtered, - lastHit: Date.now(), - hits: 1, - }); - if (cachedStyles.filters.size > 10000) { - cleanupCachedFilters(); - } - return asHash - ? Object.assign({disableAll: prefs.get('disableAll', false)}, filtered) - : filtered; + return asHash + ? Object.assign({disableAll}, cached.styles) + : cached.styles; + } + + const styles = id === null + ? (code ? cachedStyles.list : cachedStyles.noCode) + : [(cachedStyles.byId.get(id) || {})[code ? 'style' : 'noCode']]; + const filtered = asHash ? {} : []; + if (!styles) { + // may happen when users [accidentally] reopen an old URL + // of edit.html with a non-existent style id parameter + return filtered; + } + for (let i = 0, style; (style = styles[i]); i++) { + if ((enabled === null || style.enabled == enabled) + && (url === null || style.url == url) + && (id === null || style.id == id)) { + const sections = (asHash || matchUrl !== null) && getApplicableSections(style, matchUrl); + if (asHash) { + if (sections.length) { + filtered[style.id] = sections; + } + } else if (matchUrl === null || sections.length) { + filtered.push(style); + } + } + } + //console.log('%s filterStyles %s', (performance.now() - t0).toFixed(1), JSON.stringify(options)) + cachedStyles.filters.set(cacheKey, { + styles: filtered, + lastHit: Date.now(), + hits: 1, + }); + if (cachedStyles.filters.size > 10000) { + cleanupCachedFilters(); + } + return asHash + ? Object.assign({disableAll}, filtered) + : filtered; } function cleanupCachedFilters({force = false} = {}) { - if (!force) { - // sliding timer for 1 second - clearTimeout(cleanupCachedFilters.timeout); - cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true}); - return; - } - const size = cachedStyles.filters.size; - const oldestHit = cachedStyles.filters.values().next().value.lastHit; - const now = Date.now(); - const timeSpan = now - oldestHit; - const recencyWeight = 5 / size; - const hitWeight = 1 / 4; // we make ~4 hits per URL - const lastHitWeight = 10; - // delete the oldest 10% - const sorted = [...cachedStyles.filters.entries()] - .map(([id, v], index) => ({ - id, - weight: - index * recencyWeight + - v.hits * hitWeight + - (v.lastHit - oldestHit) / timeSpan * lastHitWeight, - })) - .sort((a, b) => a.weight - b.weight) - .slice(0, size / 10 + 1) - .forEach(({id}) => cachedStyles.filters.delete(id)); - cleanupCachedFilters.timeout = 0; + if (!force) { + // sliding timer for 1 second + clearTimeout(cleanupCachedFilters.timeout); + cleanupCachedFilters.timeout = setTimeout(cleanupCachedFilters, 1000, {force: true}); + return; + } + const size = cachedStyles.filters.size; + const oldestHit = cachedStyles.filters.values().next().value.lastHit; + const now = Date.now(); + const timeSpan = now - oldestHit; + const recencyWeight = 5 / size; + const hitWeight = 1 / 4; // we make ~4 hits per URL + const lastHitWeight = 10; + // delete the oldest 10% + [...cachedStyles.filters.entries()] + .map(([id, v], index) => ({ + id, + weight: + index * recencyWeight + + v.hits * hitWeight + + (v.lastHit - oldestHit) / timeSpan * lastHitWeight, + })) + .sort((a, b) => a.weight - b.weight) + .slice(0, size / 10 + 1) + .forEach(({id}) => cachedStyles.filters.delete(id)); + cleanupCachedFilters.timeout = 0; } function saveStyle(style) { - return new Promise(resolve => { - getDatabase(db => { - const tx = db.transaction(['styles'], 'readwrite'); - const os = tx.objectStore('styles'); + return new Promise(resolve => { + getDatabase(db => { + const tx = db.transaction(['styles'], 'readwrite'); + const os = tx.objectStore('styles'); - const id = style.id !== undefined && style.id !== null ? Number(style.id) : null; - const reason = style.reason; - const notify = style.notify !== false; - delete style.method; - delete style.reason; - delete style.notify; - if (!style.name) { - delete style.name; + const id = style.id !== undefined && style.id !== null ? Number(style.id) : null; + const reason = style.reason; + const notify = style.notify !== false; + delete style.method; + delete style.reason; + delete style.notify; + if (!style.name) { + delete style.name; } - // Update - if (id != null) { - style.id = id; - os.get(id).onsuccess = eventGet => { - const existed = !!eventGet.target.result; - const oldStyle = Object.assign({}, eventGet.target.result); - const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle); - style = Object.assign(oldStyle, style); - addMissingStyleTargets(style); - os.put(style).onsuccess = eventPut => { - style.id = style.id || eventPut.target.result; - invalidateCache(notify, existed ? {updated: style} : {added: style}); - if (notify) { - notifyAllTabs({ - method: existed ? 'styleUpdated' : 'styleAdded', - style, codeIsUpdated, reason, - }); - } - if (typeof handleUpdate != 'undefined') { - handleUpdate(style, {reason}); - } - resolve(style); - }; - }; - return; - } + // Update + if (id !== null) { + style.id = id; + os.get(id).onsuccess = eventGet => { + const existed = Boolean(eventGet.target.result); + const oldStyle = Object.assign({}, eventGet.target.result); + const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle); + style = Object.assign(oldStyle, style); + addMissingStyleTargets(style); + os.put(style).onsuccess = eventPut => { + style.id = style.id || eventPut.target.result; + invalidateCache(notify, existed ? {updated: style} : {added: style}); + if (notify) { + notifyAllTabs({ + method: existed ? 'styleUpdated' : 'styleAdded', + style, codeIsUpdated, reason, + }); + } + if (typeof handleUpdate != 'undefined') { + handleUpdate(style, {reason}); + } + resolve(style); + }; + }; + return; + } - // Create - delete style.id; - style = Object.assign({ - // Set optional things if they're undefined - enabled: true, - updateUrl: null, - md5Url: null, - url: null, - originalMd5: null, - }, style); - addMissingStyleTargets(style); - os.add(style).onsuccess = event => { - // Give it the ID that was generated - style.id = event.target.result; - invalidateCache(notify, {added: style}); - if (notify) { - notifyAllTabs({method: 'styleAdded', style, reason}); - } - if (typeof handleUpdate != 'undefined') { - handleUpdate(style, {reason}); - } - resolve(style); - }; - }); - }); + // Create + delete style.id; + style = Object.assign({ + // Set optional things if they're undefined + enabled: true, + updateUrl: null, + md5Url: null, + url: null, + originalMd5: null, + }, style); + addMissingStyleTargets(style); + os.add(style).onsuccess = event => { + // Give it the ID that was generated + style.id = event.target.result; + invalidateCache(notify, {added: style}); + if (notify) { + notifyAllTabs({method: 'styleAdded', style, reason}); + } + if (typeof handleUpdate != 'undefined') { + handleUpdate(style, {reason}); + } + resolve(style); + }; + }); + }); } function addMissingStyleTargets(style) { - style.sections = (style.sections || []).map(section => - Object.assign({ - urls: [], - urlPrefixes: [], - domains: [], - regexps: [], - }, section) - ); + style.sections = (style.sections || []).map(section => + Object.assign({ + urls: [], + urlPrefixes: [], + domains: [], + regexps: [], + }, section) + ); } function enableStyle(id, enabled) { - return saveStyle({id, enabled}); + return saveStyle({id, enabled}); } function deleteStyle(id, {notify = true} = {}) { - return new Promise(resolve => - getDatabase(db => { - const tx = db.transaction(['styles'], 'readwrite'); - const os = tx.objectStore('styles'); - os.delete(Number(id)).onsuccess = event => { - invalidateCache(notify, {deletedId: id}); - if (notify) { - notifyAllTabs({method: 'styleDeleted', id}); - } - if (typeof handleDelete != 'undefined') { - handleDelete(id); + return new Promise(resolve => + getDatabase(db => { + const tx = db.transaction(['styles'], 'readwrite'); + const os = tx.objectStore('styles'); + os.delete(Number(id)).onsuccess = () => { + invalidateCache(notify, {deletedId: id}); + if (notify) { + notifyAllTabs({method: 'styleDeleted', id}); } - resolve(id); - }; - })); + if (typeof handleDelete != 'undefined') { + handleDelete(id); + } + resolve(id); + }; + })); } -function reportError() { - for (i in arguments) { - if ("message" in arguments[i]) { - //alert(arguments[i].message); - console.log(arguments[i].message); - } - } +function reportError(...args) { + for (const arg of args) { + if ('message' in arg) { + console.log(arg.message); + } + } } function fixBoolean(b) { - if (typeof b != "undefined") { - return b != "false"; - } - return null; + if (typeof b != 'undefined') { + return b != 'false'; + } + return null; } function getDomains(url) { - if (url.indexOf("file:") == 0) { - return []; - } - var d = /.*?:\/*([^\/:]+)/.exec(url)[1]; - var domains = [d]; - while (d.indexOf(".") != -1) { - d = d.substring(d.indexOf(".") + 1); - domains.push(d); - } - return domains; + if (url.indexOf('file:') == 0) { + return []; + } + let d = /.*?:\/*([^/:]+)/.exec(url)[1]; + const domains = [d]; + while (d.indexOf('.') != -1) { + d = d.substring(d.indexOf('.') + 1); + domains.push(d); + } + return domains; } function getType(o) { - if (typeof o == 'undefined' || typeof o == 'string') { - return typeof o; - } - // with the persistent cachedStyles the Array reference is usually different - // so let's check for e.g. type of 'every' which is only present on arrays - // (in the context of our extension) - if (o instanceof Array || typeof o.every == 'function') { - return 'array'; - } - console.warn('Unsupported type:', o); - return 'undefined'; + if (typeof o == 'undefined' || typeof o == 'string') { + return typeof o; + } + // with the persistent cachedStyles the Array reference is usually different + // so let's check for e.g. type of 'every' which is only present on arrays + // (in the context of our extension) + if (o instanceof Array || typeof o.every == 'function') { + return 'array'; + } + console.warn('Unsupported type:', o); + return 'undefined'; } const namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/; function getApplicableSections(style, url) { - var sections = style.sections.filter(function(section) { - return sectionAppliesToUrl(section, url); - }); - // ignore if it's just namespaces - if (sections.length == 1 && namespacePattern.test(sections[0].code)) { - return []; - } - return sections; + const sections = style.sections.filter(function(section) { + return sectionAppliesToUrl(section, url); + }); + // ignore if it's just namespaces + if (sections.length == 1 && namespacePattern.test(sections[0].code)) { + return []; + } + return sections; } function sectionAppliesToUrl(section, url) { - // only http, https, file, and chrome-extension allowed - if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) { - return false; - } - // other extensions can't be styled - if (url.indexOf("chrome-extension") == 0 && url.indexOf(chrome.extension.getURL("")) != 0) { - return false; - } - 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; - } - if (section.urls.indexOf(url) != -1) { - //console.log(section.id + " applies to " + url + " due to URL rules"); - return true; - } - if (section.urlPrefixes.some(function(prefix) { - return url.indexOf(prefix) == 0; - })) { - //console.log(section.id + " applies to " + url + " due to URL prefix rules"); - return true; - } - if (section.domains.length > 0 && getDomains(url).some(function(domain) { - return section.domains.indexOf(domain) != -1; - })) { - //console.log(section.id + " applies due to " + url + " due to domain rules"); - return true; - } - if (section.regexps.some(function(regexp) { - // we want to match the full url, so add ^ and $ if not already present - if (regexp[0] != "^") { - regexp = "^" + regexp; - } - if (regexp[regexp.length - 1] != "$") { - regexp += "$"; - } - 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; - } - //console.log(section.id + " does not apply due to " + url); - return false; + // only http, https, file, ftp, and chrome-extension://OWN_EXTENSION_ID allowed + if (!url.startsWith('http') + && !url.startsWith('ftp') + && !url.startsWith('file') + && !url.startsWith(OWN_ORIGIN)) { + return false; + } + if (section.urls.length == 0 + && section.domains.length == 0 + && section.urlPrefixes.length == 0 + && section.regexps.length == 0) { + return true; + } + if (section.urls.indexOf(url) != -1) { + return true; + } + for (const urlPrefix of section.urlPrefixes) { + if (url.startsWith(urlPrefix)) { + return true; + } + } + if (section.domains.length) { + for (const domain of getDomains(url)) { + if (section.domains.indexOf(domain) != -1) { + return true; + } + } + } + for (const regexp of section.regexps) { + // we want to match the full url, so add ^ and $ if not already present + const prefix = regexp.charAt(0) != '^' && '^'; + const suffix = regexp.slice(-1) != '$' && '$'; + const re = runTryCatch(() => new RegExp(prefix + regexp + suffix)); + if (!re) { + console.warn('Regexp ' + regexp + ' is not valid'); + } else if (re.test(url)) { + return true; + } + } + return false; } 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 // so we should keep it isolated from normal code in a minimal wrapper -function runTryCatch(func) { - try { return func() } - catch(e) {} +// Update: might get fixed in V8 TurboFan in the future +function runTryCatch(func, ...args) { + try { + return func(...args); + } catch (e) {} } +prefs = prefs || new function Prefs() { + const me = this; + + const defaults = { + 'openEditInWindow': false, // new editor opens in a own browser window + 'windowPosition': {}, // detached window position + 'show-badge': true, // display text on popup menu icon + 'disableAll': false, // boss key + + 'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs + 'popup.breadcrumbs.usePath': false, // use URL path for 'this URL' + 'popup.enabledFirst': true, // display enabled styles before disabled styles + 'popup.stylesFirst': true, // display enabled styles before disabled styles + + 'manage.onlyEnabled': false, // display only enabled styles + 'manage.onlyEdited': false, // display only styles created locally + + 'editor.options': {}, // CodeMirror.defaults.* + 'editor.lineWrapping': true, // word wrap + 'editor.smartIndent': true, // 'smart' indent + 'editor.indentWithTabs': false, // smart indent with tabs + 'editor.tabSize': 4, // tab width, in spaces + 'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default', + 'editor.theme': 'default', // CSS theme + 'editor.beautify': { // CSS beautifier + selector_separator_newline: true, + newline_before_open_brace: false, + newline_after_open_brace: true, + newline_between_properties: true, + newline_before_close_brace: true, + newline_between_rules: false, + end_with_newline: false + }, + 'editor.lintDelay': 500, // lint gutter marker update delay, ms + 'editor.lintReportDelay': 4500, // lint report update delay, ms + + 'badgeDisabled': '#8B0000', // badge background color when disabled + 'badgeNormal': '#006666', // badge background color + + 'popupWidth': 240, // popup width in pixels + + 'updateInterval': 0 // user-style automatic update interval, hour + }; + const values = deepCopy(defaults); + + let syncTimeout; // see broadcast() function below + + Object.defineProperty(this, 'readOnlyValues', {value: {}}); + + Prefs.prototype.get = function(key, defaultValue) { + if (key in values) { + return values[key]; + } + if (defaultValue !== undefined) { + return defaultValue; + } + if (key in defaults) { + return defaults[key]; + } + console.warn("No default preference for '%s'", key); + }; + + Prefs.prototype.getAll = function() { + return deepCopy(values); + }; + + Prefs.prototype.set = function(key, value, options) { + const oldValue = deepCopy(values[key]); + values[key] = value; + defineReadonlyProperty(this.readOnlyValues, key, value); + if ((!options || !options.noBroadcast) && !equal(value, oldValue)) { + me.broadcast(key, value, options); + } + }; + + Prefs.prototype.remove = key => me.set(key, undefined); + + Prefs.prototype.broadcast = function(key, value, options) { + const message = {method: 'prefChanged', prefName: key, value: value}; + notifyAllTabs(message); + chrome.runtime.sendMessage(message); + if (key == 'disableAll') { + notifyAllTabs({method: 'styleDisableAll', disableAll: value}); + } + if (!options || !options.noSync) { + clearTimeout(syncTimeout); + syncTimeout = setTimeout(function() { + getSync().set({'settings': values}); + }, 0); + } + }; + + Object.keys(defaults).forEach(function(key) { + me.set(key, defaults[key], {noBroadcast: true}); + }); + + getSync().get('settings', function(result) { + const synced = result.settings; + for (const key in defaults) { + if (synced && (key in synced)) { + me.set(key, synced[key], {noSync: true}); + } else { + const value = tryMigrating(key); + if (value !== undefined) { + me.set(key, value); + } + } + } + if (typeof contextMenus !== 'undefined') { + for (const id in contextMenus) { + if (typeof values[id] == 'boolean') { + me.broadcast(id, values[id], {noSync: true}); + } + } + } + }); + + chrome.storage.onChanged.addListener(function(changes, area) { + if (area == 'sync' && 'settings' in changes) { + const synced = changes.settings.newValue; + if (synced) { + for (const key in defaults) { + if (key in synced) { + me.set(key, synced[key], {noSync: true}); + } + } + } else { + // user manually deleted our settings, we'll recreate them + getSync().set({'settings': values}); + } + } + }); + + function tryMigrating(key) { + if (!(key in localStorage)) { + return undefined; + } + const value = localStorage[key]; + delete localStorage[key]; + localStorage['DEPRECATED: ' + key] = value; + switch (typeof defaults[key]) { + case 'boolean': + return value.toLowerCase() === 'true'; + case 'number': + return Number(value); + case 'object': + try { + return JSON.parse(value); + } catch (e) { + console.log("Cannot migrate from localStorage %s = '%s': %o", key, value, e); + return undefined; + } + } + 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) { - 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; - } + 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; + } } -var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() { - var me = this; - - var defaults = { - "openEditInWindow": false, // new editor opens in a own browser window - "windowPosition": {}, // detached window position - "show-badge": true, // display text on popup menu icon - "disableAll": false, // boss key - - "popup.breadcrumbs": true, // display "New style" links as URL breadcrumbs - "popup.breadcrumbs.usePath": false, // use URL path for "this URL" - "popup.enabledFirst": true, // display enabled styles before disabled styles - "popup.stylesFirst": true, // display enabled styles before disabled styles - - "manage.onlyEnabled": false, // display only enabled styles - "manage.onlyEdited": false, // display only styles created locally - - "editor.options": {}, // CodeMirror.defaults.* - "editor.lineWrapping": true, // word wrap - "editor.smartIndent": true, // "smart" indent - "editor.indentWithTabs": false, // smart indent with tabs - "editor.tabSize": 4, // tab width, in spaces - "editor.keyMap": navigator.appVersion.indexOf("Windows") > 0 ? "sublime" : "default", - "editor.theme": "default", // CSS theme - "editor.beautify": { // CSS beautifier - selector_separator_newline: true, - newline_before_open_brace: false, - newline_after_open_brace: true, - newline_between_properties: true, - newline_before_close_brace: true, - newline_between_rules: false, - end_with_newline: false - }, - "editor.lintDelay": 500, // lint gutter marker update delay, ms - "editor.lintReportDelay": 4500, // lint report update delay, ms - - "badgeDisabled": "#8B0000", // badge background color when disabled - "badgeNormal": "#006666", // badge background color - - "popupWidth": 240, // popup width in pixels - - "updateInterval": 0 // user-style automatic update interval, hour - }; - var values = deepCopy(defaults); - - var syncTimeout; // see broadcast() function below - - Object.defineProperty(this, "readOnlyValues", {value: {}}); - - Prefs.prototype.get = function(key, defaultValue) { - if (key in values) { - return values[key]; - } - if (defaultValue !== undefined) { - return defaultValue; - } - if (key in defaults) { - return defaults[key]; - } - console.warn("No default preference for '%s'", key); - }; - - Prefs.prototype.getAll = function(key) { - return deepCopy(values); - }; - - Prefs.prototype.set = function(key, value, options) { - var oldValue = deepCopy(values[key]); - values[key] = value; - defineReadonlyProperty(this.readOnlyValues, key, value); - if ((!options || !options.noBroadcast) && !equal(value, oldValue)) { - me.broadcast(key, value, options); - } - }; - - Prefs.prototype.remove = function(key) { me.set(key, undefined) }; - - Prefs.prototype.broadcast = function(key, value, options) { - var message = {method: "prefChanged", prefName: key, value: value}; - notifyAllTabs(message); - chrome.runtime.sendMessage(message); - if (key == "disableAll") { - notifyAllTabs({method: "styleDisableAll", disableAll: value}); - } - if (!options || !options.noSync) { - clearTimeout(syncTimeout); - syncTimeout = setTimeout(function() { - getSync().set({"settings": values}); - }, 0); - } - }; - - Object.keys(defaults).forEach(function(key) { - me.set(key, defaults[key], {noBroadcast: true}); - }); - - getSync().get("settings", function(result) { - var synced = result.settings; - for (var key in defaults) { - if (synced && (key in synced)) { - me.set(key, synced[key], {noSync: true}); - } else { - var value = tryMigrating(key); - if (value !== undefined) { - me.set(key, value); - } - } - } - if (typeof contextMenus !== 'undefined') { - for (let id in contextMenus) { - if (typeof values[id] == 'boolean') { - me.broadcast(id, values[id], {noSync: true}); - } - } - } - }); - - chrome.storage.onChanged.addListener(function(changes, area) { - if (area == "sync" && "settings" in changes) { - var synced = changes.settings.newValue; - if (synced) { - for (key in defaults) { - if (key in synced) { - me.set(key, synced[key], {noSync: true}); - } - } - } else { - // user manually deleted our settings, we'll recreate them - getSync().set({"settings": values}); - } - } - }); - - function tryMigrating(key) { - if (!(key in localStorage)) { - return undefined; - } - var value = localStorage[key]; - delete localStorage[key]; - localStorage["DEPRECATED: " + key] = value; - switch (typeof defaults[key]) { - case "boolean": - return value.toLowerCase() === "true"; - case "number": - return Number(value); - case "object": - try { - return JSON.parse(value); - } catch(e) { - console.log("Cannot migrate from localStorage %s = '%s': %o", key, value, e); - return undefined; - } - } - return value; - } -}; - function getCodeMirrorThemes(callback) { - chrome.runtime.getPackageDirectoryEntry(function(rootDir) { - rootDir.getDirectory("codemirror/theme", {create: false}, function(themeDir) { - themeDir.createReader().readEntries(function(entries) { - var themes = [chrome.i18n.getMessage("defaultTheme")]; - entries - .filter(function(entry) { return entry.isFile }) - .sort(function(a, b) { return a.name < b.name ? -1 : 1 }) - .forEach(function(entry) { - themes.push(entry.name.replace(/\.css$/, "")); - }); - if (callback) { - callback(themes); - } - }); - }); - }); + chrome.runtime.getPackageDirectoryEntry(function(rootDir) { + rootDir.getDirectory('codemirror/theme', {create: false}, function(themeDir) { + themeDir.createReader().readEntries(function(entries) { + const themes = [chrome.i18n.getMessage('defaultTheme')]; + entries + .filter(entry => entry.isFile) + .sort((a, b) => (a.name < b.name ? -1 : 1)) + .forEach(function(entry) { + themes.push(entry.name.replace(/\.css$/, '')); + }); + if (callback) { + callback(themes); + } + }); + }); + }); } function sessionStorageHash(name) { - var hash = { - value: {}, - set: function(k, v) { this.value[k] = v; this.updateStorage(); }, - unset: function(k) { delete this.value[k]; this.updateStorage(); }, - updateStorage: function() { - sessionStorage[this.name] = JSON.stringify(this.value); - } - }; - try { hash.value = JSON.parse(sessionStorage[name]); } catch(e) {} - Object.defineProperty(hash, "name", {value: name}); - return hash; + return { + name, + value: runTryCatch(JSON.parse, sessionStorage[name]) || {}, + set(k, v) { + this.value[k] = v; + this.updateStorage(); + }, + unset(k) { + delete this.value[k]; + this.updateStorage(); + }, + updateStorage() { + sessionStorage[this.name] = JSON.stringify(this.value); + } + }; } function deepCopy(obj) { - if (!obj || typeof obj != "object") { - return obj; - } else { - var emptyCopy = Object.create(Object.getPrototypeOf(obj)); - return deepMerge(emptyCopy, obj); - } + if (!obj || typeof obj != 'object') { + return obj; + } else { + const emptyCopy = Object.create(Object.getPrototypeOf(obj)); + return deepMerge(emptyCopy, obj); + } } -function deepMerge(target, obj1 /* plus any number of object arguments */) { - for (var i = 1; i < arguments.length; i++) { - var obj = arguments[i]; - for (var k in obj) { - // hasOwnProperty checking is not needed for our non-OOP stuff - var value = obj[k]; - if (!value || typeof value != "object") { - target[k] = value; - } else if (k in target) { - deepMerge(target[k], value); - } else { - target[k] = deepCopy(value); - } - } - } - return target; +function deepMerge(target, ...args) { + for (const obj of args) { + for (const k in obj) { + const value = obj[k]; + if (!value || typeof value != 'object') { + target[k] = value; + } else if (k in target) { + deepMerge(target[k], value); + } else if (typeof value.slice == 'function') { + target[k] = value.slice(); + } else { + target[k] = deepCopy(value); + } + } + } + return target; } function equal(a, b) { - if (!a || !b || typeof a != "object" || typeof b != "object") { - return a === b; - } - if (Object.keys(a).length != Object.keys(b).length) { - return false; - } - for (var k in a) { - if (a[k] !== b[k]) { - return false; - } - } - return true; + if (!a || !b || typeof a != 'object' || typeof b != 'object') { + return a === b; + } + if (Object.keys(a).length != Object.keys(b).length) { + return false; + } + for (const k in a) { + if (a[k] !== b[k]) { + return false; + } + } + return true; } function defineReadonlyProperty(obj, key, value) { - var 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") { - Object.freeze(copy); - } - Object.defineProperty(obj, key, {value: copy, configurable: true}) + const copy = deepCopy(value); + if (typeof copy == 'object') { + Object.freeze(copy); + } + Object.defineProperty(obj, key, {value: copy, configurable: true}); } // Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 function getSync() { - if ("sync" in chrome.storage) { - return chrome.storage.sync; - } - crappyStorage = {}; - return { - get: function(key, callback) { - callback(crappyStorage[key] || {}); - }, - set: function(source, callback) { - for (var property in source) { - if (source.hasOwnProperty(property)) { - crappyStorage[property] = source[property]; - } - } - callback(); - } - } + if ('sync' in chrome.storage) { + return chrome.storage.sync; + } + const crappyStorage = {}; + return { + get(key, callback) { + callback(crappyStorage[key] || {}); + }, + set(source, callback) { + for (const property in source) { + if (source.hasOwnProperty(property)) { + crappyStorage[property] = source[property]; + } + } + callback(); + } + }; } function styleSectionsEqual(styleA, styleB) { - if (!styleA.sections || !styleB.sections) { - return undefined; - } - if (styleA.sections.length != styleB.sections.length) { - return false; - } - const propNames = ['code', 'urlPrefixes', 'urls', 'domains', 'regexps']; - const typeBcaches = []; - checkingEveryInA: for (let sectionA of styleA.sections) { - const typeAcache = new Map(); - for (let name of propNames) { - typeAcache.set(name, getType(sectionA[name])); - } - lookingForDupeInB: for (let i = 0, sectionB; (sectionB = styleB.sections[i]); i++) { - const typeBcache = typeBcaches[i] = typeBcaches[i] || new Map(); - comparingProps: for (let name of propNames) { - const propA = sectionA[name], typeA = typeAcache.get(name); - const propB = sectionB[name]; - let typeB = typeBcache.get(name); - if (!typeB) { - typeB = getType(propB); - typeBcache.set(name, typeB); - } - if (typeA != typeB) { - const bothEmptyOrUndefined = - (typeA == 'undefined' || (typeA == 'array' && propA.length == 0)) && - (typeB == 'undefined' || (typeB == 'array' && propB.length == 0)); - if (bothEmptyOrUndefined) { - continue comparingProps; - } else { - continue lookingForDupeInB; - } - } - if (typeA == 'undefined') { - continue comparingProps; - } - if (typeA == 'array') { - if (propA.length != propB.length) { - continue lookingForDupeInB; - } - for (let item of propA) { - if (propB.indexOf(item) < 0) { - continue lookingForDupeInB; - } - } - continue comparingProps; - } - if (typeA == 'string' && propA != propB) { - continue lookingForDupeInB; - } - } - // dupe found - continue checkingEveryInA; - } - // dupe not found - return false; - } - return true; + if (!styleA.sections || !styleB.sections) { + return undefined; + } + if (styleA.sections.length != styleB.sections.length) { + return false; + } + const propNames = ['code', 'urlPrefixes', 'urls', 'domains', 'regexps']; + const typeBcaches = []; + checkingEveryInA: + for (const sectionA of styleA.sections) { + const typeAcache = new Map(); + for (const name of propNames) { + typeAcache.set(name, getType(sectionA[name])); + } + lookingForDupeInB: + for (let i = 0, sectionB; (sectionB = styleB.sections[i]); i++) { + const typeBcache = typeBcaches[i] = typeBcaches[i] || new Map(); + comparingProps: + for (const name of propNames) { + const propA = sectionA[name]; + const typeA = typeAcache.get(name); + const propB = sectionB[name]; + let typeB = typeBcache.get(name); + if (!typeB) { + typeB = getType(propB); + typeBcache.set(name, typeB); + } + if (typeA != typeB) { + const bothEmptyOrUndefined = + (typeA == 'undefined' || (typeA == 'array' && propA.length == 0)) && + (typeB == 'undefined' || (typeB == 'array' && propB.length == 0)); + if (bothEmptyOrUndefined) { + continue comparingProps; + } else { + continue lookingForDupeInB; + } + } + if (typeA == 'undefined') { + continue comparingProps; + } + if (typeA == 'array') { + if (propA.length != propB.length) { + continue lookingForDupeInB; + } + for (const item of propA) { + if (propB.indexOf(item) < 0) { + continue lookingForDupeInB; + } + } + continue comparingProps; + } + if (typeA == 'string' && propA != propB) { + continue lookingForDupeInB; + } + } + // dupe found + continue checkingEveryInA; + } + // dupe not found + return false; + } + return true; } diff --git a/update.js b/update.js index e974471f..d74290ab 100644 --- a/update.js +++ b/update.js @@ -1,6 +1,7 @@ -/* globals getStyles, saveStyle, prefs */ +/* globals getStyles */ 'use strict'; +// TODO: refactor to make usable in manage::Updater var update = { fetch: (resource, callback) => { let req = new XMLHttpRequest(); @@ -30,6 +31,7 @@ var update = { getStyles({}, (styles) => callback(styles.filter(style => style.updateUrl))); }, perform: (observe = function () {}) => { + // TODO: use sectionsAreEqual // from install.js function arraysAreEqual (a, b) { // treat empty array and undefined as equivalent