diff --git a/.eslintignore b/.eslintignore index 325be71d..a710e413 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,2 @@ -beautify/ -codemirror/ -csslint/ +vendor/ +vendor-overwrites/ diff --git a/background.js b/background/background.js similarity index 100% rename from background.js rename to background/background.js diff --git a/messaging.js b/background/messaging.js similarity index 96% rename from messaging.js rename to background/messaging.js index 42f8b593..ac529c92 100644 --- a/messaging.js +++ b/background/messaging.js @@ -1,371 +1,371 @@ -/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */ -'use strict'; - -// keep message channel open for sendResponse in chrome.runtime.onMessage listener -const KEEP_CHANNEL_OPEN = true; - -const FIREFOX = /Firefox/.test(navigator.userAgent); -const OPERA = /OPR/.test(navigator.userAgent); - -const URLS = { - ownOrigin: chrome.runtime.getURL(''), - - optionsUI: [ - chrome.runtime.getURL('options/index.html'), - 'chrome://extensions/?options=' + chrome.runtime.id, - ], - - configureCommands: - OPERA ? 'opera://settings/configureCommands' - : 'chrome://extensions/configureCommands', - - // CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL - // https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc - chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : ( - OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/' - ), - - supported: new RegExp( - '^(file|ftps?|http)://|' + - `^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : ( - OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)' - )}|` + - '^' + chrome.runtime.getURL('')), -}; - -let BG = chrome.extension.getBackgroundPage(); - -if (!BG || BG != window) { - document.documentElement.classList.toggle('firefox', FIREFOX); - document.documentElement.classList.toggle('opera', OPERA); - // TODO: remove once our manifest's minimum_chrome_version is 50+ - // Chrome 49 doesn't report own extension pages in webNavigation apparently - if (navigator.userAgent.includes('Chrome/49.')) { - getActiveTab().then(BG.updateIcon); - } -} - -function notifyAllTabs(msg) { - const originalMessage = msg; - if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') { - // apply/popup/manage use only meta for these two methods, - // editor may need the full code but can fetch it directly, - // so we send just the meta to avoid spamming lots of tabs with huge styles - msg = Object.assign({}, msg, { - style: getStyleWithNoCode(msg.style) - }); - } - const affectsAll = !msg.affects || msg.affects.all; - const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager); - const affectsTabs = affectsAll || affectsOwnOriginOnly; - const affectsIcon = affectsAll || msg.affects.icon; - const affectsPopup = affectsAll || msg.affects.popup; - const affectsSelf = affectsPopup || msg.prefs; - if (affectsTabs || affectsIcon) { - const notifyTab = tab => { - // own pages will be notified via runtime.sendMessage later - if ((affectsTabs || URLS.optionsUI.includes(tab.url)) - && !(affectsSelf && tab.url.startsWith(URLS.ownOrigin)) - // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF - && (!FIREFOX || tab.width)) { - chrome.tabs.sendMessage(tab.id, msg); - } - if (affectsIcon && BG) { - BG.updateIcon(tab); - } - }; - // list all tabs including chrome-extension:// which can be ours - Promise.all([ - queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}), - getActiveTab(), - ]).then(([tabs, activeTab]) => { - const activeTabId = activeTab && activeTab.id; - for (const tab of tabs) { - invokeOrPostpone(tab.id === activeTabId, notifyTab, tab); - } - }); - } - // notify self: the message no longer is sent to the origin in new Chrome - if (typeof onRuntimeMessage != 'undefined') { - onRuntimeMessage(originalMessage); - } - // notify apply.js on own pages - if (typeof applyOnMessage != 'undefined') { - applyOnMessage(originalMessage); - } - // notify background page and all open popups - if (affectsSelf) { - chrome.runtime.sendMessage(msg); - } -} - - -function queryTabs(options = {}) { - return new Promise(resolve => - chrome.tabs.query(options, tabs => - resolve(tabs))); -} - - -function getTab(id) { - return new Promise(resolve => - chrome.tabs.get(id, tab => - !chrome.runtime.lastError && resolve(tab))); -} - - -function getOwnTab() { - return new Promise(resolve => - chrome.tabs.getCurrent(tab => resolve(tab))); -} - - -function getActiveTab() { - return queryTabs({currentWindow: true, active: true}) - .then(tabs => tabs[0]); -} - - -function getActiveTabRealURL() { - 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 => { - resolve(frame && 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 => { - // [some] chromium forks don't handle their fake branded protocols - url = url.replace(/^(opera|vivaldi)/, 'chrome'); - // FF doesn't handle moz-extension:// URLs (bug) - // API doesn't handle the hash-fragment part - const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, ''); - queryTabs({url: urlQuery, currentWindow}).then(tabs => { - for (const tab of tabs) { - if (tab.url == url) { - activateTab(tab).then(resolve); - return; - } - } - getActiveTab().then(tab => { - if (tab && tab.url == 'chrome://newtab/' - // prevent redirecting incognito NTP to a chrome URL as it crashes Chrome - && (!url.startsWith('chrome') || !tab.incognito)) { - chrome.tabs.update({url}, resolve); - } else { - chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {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); - }), - ]); -} - - -function stringAsRegExp(s, flags) { - return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags); -} - - -function ignoreChromeError() { - chrome.runtime.lastError; // eslint-disable-line no-unused-expressions -} - - -function getStyleWithNoCode(style) { - const stripped = Object.assign({}, style, {sections: []}); - for (const section of style.sections) { - stripped.sections.push(Object.assign({}, section, {code: null})); - } - return stripped; -} - - -// js engine can't optimize the entire function if it contains try-catch -// so we should keep it isolated from normal code in a minimal wrapper -// Update: might get fixed in V8 TurboFan in the future -function tryCatch(func, ...args) { - try { - return func(...args); - } catch (e) {} -} - - -function tryRegExp(regexp) { - try { - return new RegExp(regexp); - } catch (e) {} -} - - -function tryJSONparse(jsonString) { - try { - return JSON.parse(jsonString); - } catch (e) {} -} - - -const debounce = Object.assign((fn, delay, ...args) => { - clearTimeout(debounce.timers.get(fn)); - debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); -}, { - timers: new Map(), - run(fn, ...args) { - debounce.timers.delete(fn); - fn(...args); - }, - unregister(fn) { - clearTimeout(debounce.timers.get(fn)); - debounce.timers.delete(fn); - }, -}); - - -function deepCopy(obj) { - return obj !== null && obj !== undefined && typeof obj == 'object' - ? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj) - : obj; -} - - -function deepMerge(target, ...args) { - const isArray = typeof target.slice == 'function'; - for (const obj of args) { - if (isArray && obj !== null && obj !== undefined) { - for (const element of obj) { - target.push(deepCopy(element)); - } - continue; - } - for (const k in obj) { - const value = obj[k]; - if (k in target && typeof value == 'object' && value !== null) { - deepMerge(target[k], value); - } else { - target[k] = deepCopy(value); - } - } - } - return target; -} - - -function sessionStorageHash(name) { - return { - name, - value: tryCatch(JSON.parse, sessionStorage[name]) || {}, - set(k, v) { - this.value[k] = v; - this.updateStorage(); - }, - unset(k) { - delete this.value[k]; - this.updateStorage(); - }, - updateStorage() { - sessionStorage[this.name] = JSON.stringify(this.value); - } - }; -} - - -function onBackgroundReady() { - return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) { - chrome.runtime.sendMessage({method: 'healthCheck'}, health => { - if (health !== undefined) { - BG = chrome.extension.getBackgroundPage(); - resolve(); - } else { - setTimeout(ping, 0, resolve); - } - }); - }); -} - - -// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage -function getStylesSafe(options) { - return onBackgroundReady() - .then(() => BG.getStyles(options)); -} - - -function saveStyleSafe(style) { - return onBackgroundReady() - .then(() => BG.saveStyle(BG.deepCopy(style))) - .then(savedStyle => { - if (style.notify === false) { - handleUpdate(savedStyle, style); - } - return savedStyle; - }); -} - - -function deleteStyleSafe({id, notify = true} = {}) { - return onBackgroundReady() - .then(() => BG.deleteStyle({id, notify})) - .then(() => { - if (!notify) { - handleDelete(id); - } - return id; - }); -} - - -function download(url) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.timeout = 10e3; - xhr.onloadend = () => (xhr.status == 200 - ? resolve(xhr.responseText) - : reject(xhr.status)); - const [mainUrl, query] = url.split('?'); - xhr.open(query ? 'POST' : 'GET', mainUrl, true); - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); - xhr.send(query); - }); -} - - -function doTimeout(ms = 0, ...args) { - return ms > 0 - ? () => new Promise(resolve => setTimeout(resolve, ms, ...args)) - : new Promise(resolve => setTimeout(resolve, 0, ...args)); -} - - -function invokeOrPostpone(isInvoke, fn, ...args) { - return isInvoke - ? fn(...args) - : setTimeout(invokeOrPostpone, 0, true, fn, ...args); -} +/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */ +'use strict'; + +// keep message channel open for sendResponse in chrome.runtime.onMessage listener +const KEEP_CHANNEL_OPEN = true; + +const FIREFOX = /Firefox/.test(navigator.userAgent); +const OPERA = /OPR/.test(navigator.userAgent); + +const URLS = { + ownOrigin: chrome.runtime.getURL(''), + + optionsUI: [ + chrome.runtime.getURL('options/index.html'), + 'chrome://extensions/?options=' + chrome.runtime.id, + ], + + configureCommands: + OPERA ? 'opera://settings/configureCommands' + : 'chrome://extensions/configureCommands', + + // CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL + // https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc + chromeWebStore: FIREFOX ? 'https://addons.mozilla.org/' : ( + OPERA ? 'https://addons.opera.com/' : 'https://chrome.google.com/webstore/' + ), + + supported: new RegExp( + '^(file|ftps?|http)://|' + + `^https://${FIREFOX ? '(?!addons\\.mozilla\\.org)' : ( + OPERA ? '(?!addons\\.opera\\.com)' : '(?!chrome\\.google\\.com/webstore)' + )}|` + + '^' + chrome.runtime.getURL('')), +}; + +let BG = chrome.extension.getBackgroundPage(); + +if (!BG || BG != window) { + document.documentElement.classList.toggle('firefox', FIREFOX); + document.documentElement.classList.toggle('opera', OPERA); + // TODO: remove once our manifest's minimum_chrome_version is 50+ + // Chrome 49 doesn't report own extension pages in webNavigation apparently + if (navigator.userAgent.includes('Chrome/49.')) { + getActiveTab().then(BG.updateIcon); + } +} + +function notifyAllTabs(msg) { + const originalMessage = msg; + if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') { + // apply/popup/manage use only meta for these two methods, + // editor may need the full code but can fetch it directly, + // so we send just the meta to avoid spamming lots of tabs with huge styles + msg = Object.assign({}, msg, { + style: getStyleWithNoCode(msg.style) + }); + } + const affectsAll = !msg.affects || msg.affects.all; + const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager); + const affectsTabs = affectsAll || affectsOwnOriginOnly; + const affectsIcon = affectsAll || msg.affects.icon; + const affectsPopup = affectsAll || msg.affects.popup; + const affectsSelf = affectsPopup || msg.prefs; + if (affectsTabs || affectsIcon) { + const notifyTab = tab => { + // own pages will be notified via runtime.sendMessage later + if ((affectsTabs || URLS.optionsUI.includes(tab.url)) + && !(affectsSelf && tab.url.startsWith(URLS.ownOrigin)) + // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF + && (!FIREFOX || tab.width)) { + chrome.tabs.sendMessage(tab.id, msg); + } + if (affectsIcon && BG) { + BG.updateIcon(tab); + } + }; + // list all tabs including chrome-extension:// which can be ours + Promise.all([ + queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}), + getActiveTab(), + ]).then(([tabs, activeTab]) => { + const activeTabId = activeTab && activeTab.id; + for (const tab of tabs) { + invokeOrPostpone(tab.id === activeTabId, notifyTab, tab); + } + }); + } + // notify self: the message no longer is sent to the origin in new Chrome + if (typeof onRuntimeMessage != 'undefined') { + onRuntimeMessage(originalMessage); + } + // notify apply.js on own pages + if (typeof applyOnMessage != 'undefined') { + applyOnMessage(originalMessage); + } + // notify background page and all open popups + if (affectsSelf) { + chrome.runtime.sendMessage(msg); + } +} + + +function queryTabs(options = {}) { + return new Promise(resolve => + chrome.tabs.query(options, tabs => + resolve(tabs))); +} + + +function getTab(id) { + return new Promise(resolve => + chrome.tabs.get(id, tab => + !chrome.runtime.lastError && resolve(tab))); +} + + +function getOwnTab() { + return new Promise(resolve => + chrome.tabs.getCurrent(tab => resolve(tab))); +} + + +function getActiveTab() { + return queryTabs({currentWindow: true, active: true}) + .then(tabs => tabs[0]); +} + + +function getActiveTabRealURL() { + 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 => { + resolve(frame && 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 => { + // [some] chromium forks don't handle their fake branded protocols + url = url.replace(/^(opera|vivaldi)/, 'chrome'); + // FF doesn't handle moz-extension:// URLs (bug) + // API doesn't handle the hash-fragment part + const urlQuery = url.startsWith('moz-extension') ? undefined : url.replace(/#.*/, ''); + queryTabs({url: urlQuery, currentWindow}).then(tabs => { + for (const tab of tabs) { + if (tab.url == url) { + activateTab(tab).then(resolve); + return; + } + } + getActiveTab().then(tab => { + if (tab && tab.url == 'chrome://newtab/' + // prevent redirecting incognito NTP to a chrome URL as it crashes Chrome + && (!url.startsWith('chrome') || !tab.incognito)) { + chrome.tabs.update({url}, resolve); + } else { + chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {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); + }), + ]); +} + + +function stringAsRegExp(s, flags) { + return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags); +} + + +function ignoreChromeError() { + chrome.runtime.lastError; // eslint-disable-line no-unused-expressions +} + + +function getStyleWithNoCode(style) { + const stripped = Object.assign({}, style, {sections: []}); + for (const section of style.sections) { + stripped.sections.push(Object.assign({}, section, {code: null})); + } + return stripped; +} + + +// js engine can't optimize the entire function if it contains try-catch +// so we should keep it isolated from normal code in a minimal wrapper +// Update: might get fixed in V8 TurboFan in the future +function tryCatch(func, ...args) { + try { + return func(...args); + } catch (e) {} +} + + +function tryRegExp(regexp) { + try { + return new RegExp(regexp); + } catch (e) {} +} + + +function tryJSONparse(jsonString) { + try { + return JSON.parse(jsonString); + } catch (e) {} +} + + +const debounce = Object.assign((fn, delay, ...args) => { + clearTimeout(debounce.timers.get(fn)); + debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args)); +}, { + timers: new Map(), + run(fn, ...args) { + debounce.timers.delete(fn); + fn(...args); + }, + unregister(fn) { + clearTimeout(debounce.timers.get(fn)); + debounce.timers.delete(fn); + }, +}); + + +function deepCopy(obj) { + return obj !== null && obj !== undefined && typeof obj == 'object' + ? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj) + : obj; +} + + +function deepMerge(target, ...args) { + const isArray = typeof target.slice == 'function'; + for (const obj of args) { + if (isArray && obj !== null && obj !== undefined) { + for (const element of obj) { + target.push(deepCopy(element)); + } + continue; + } + for (const k in obj) { + const value = obj[k]; + if (k in target && typeof value == 'object' && value !== null) { + deepMerge(target[k], value); + } else { + target[k] = deepCopy(value); + } + } + } + return target; +} + + +function sessionStorageHash(name) { + return { + name, + value: tryCatch(JSON.parse, sessionStorage[name]) || {}, + set(k, v) { + this.value[k] = v; + this.updateStorage(); + }, + unset(k) { + delete this.value[k]; + this.updateStorage(); + }, + updateStorage() { + sessionStorage[this.name] = JSON.stringify(this.value); + } + }; +} + + +function onBackgroundReady() { + return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) { + chrome.runtime.sendMessage({method: 'healthCheck'}, health => { + if (health !== undefined) { + BG = chrome.extension.getBackgroundPage(); + resolve(); + } else { + setTimeout(ping, 0, resolve); + } + }); + }); +} + + +// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage +function getStylesSafe(options) { + return onBackgroundReady() + .then(() => BG.getStyles(options)); +} + + +function saveStyleSafe(style) { + return onBackgroundReady() + .then(() => BG.saveStyle(BG.deepCopy(style))) + .then(savedStyle => { + if (style.notify === false) { + handleUpdate(savedStyle, style); + } + return savedStyle; + }); +} + + +function deleteStyleSafe({id, notify = true} = {}) { + return onBackgroundReady() + .then(() => BG.deleteStyle({id, notify})) + .then(() => { + if (!notify) { + handleDelete(id); + } + return id; + }); +} + + +function download(url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.timeout = 10e3; + xhr.onloadend = () => (xhr.status == 200 + ? resolve(xhr.responseText) + : reject(xhr.status)); + const [mainUrl, query] = url.split('?'); + xhr.open(query ? 'POST' : 'GET', mainUrl, true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send(query); + }); +} + + +function doTimeout(ms = 0, ...args) { + return ms > 0 + ? () => new Promise(resolve => setTimeout(resolve, ms, ...args)) + : new Promise(resolve => setTimeout(resolve, 0, ...args)); +} + + +function invokeOrPostpone(isInvoke, fn, ...args) { + return isInvoke + ? fn(...args) + : setTimeout(invokeOrPostpone, 0, true, fn, ...args); +} diff --git a/storage.js b/background/storage.js similarity index 100% rename from storage.js rename to background/storage.js diff --git a/update.js b/background/update.js similarity index 100% rename from update.js rename to background/update.js diff --git a/backup/fileSaveLoad.js b/backup/fileSaveLoad.js deleted file mode 100644 index 7626a1b2..00000000 --- a/backup/fileSaveLoad.js +++ /dev/null @@ -1,395 +0,0 @@ -/* global messageBox, handleUpdate, applyOnMessage */ -'use strict'; - -const STYLISH_DUMP_FILE_EXT = '.txt'; -const STYLUS_BACKUP_FILE_EXT = '.json'; - - -function importFromFile({fileTypeFilter, file} = {}) { - return new Promise(resolve => { - const fileInput = document.createElement('input'); - if (file) { - readFile(); - return; - } - fileInput.style.display = 'none'; - fileInput.type = 'file'; - fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT; - fileInput.acceptCharset = 'utf-8'; - - document.body.appendChild(fileInput); - fileInput.initialValue = fileInput.value; - fileInput.onchange = readFile; - fileInput.click(); - - function readFile() { - if (file || fileInput.value !== fileInput.initialValue) { - file = file || fileInput.files[0]; - if (file.size > 100e6) { - console.warn("100MB backup? I don't believe you."); - importFromString('').then(resolve); - return; - } - document.body.style.cursor = 'wait'; - const fReader = new FileReader(); - fReader.onloadend = event => { - fileInput.remove(); - importFromString(event.target.result).then(numStyles => { - document.body.style.cursor = ''; - resolve(numStyles); - }); - }; - fReader.readAsText(file, 'utf-8'); - } - } - }); -} - - -function importFromString(jsonString) { - if (!BG) { - onBackgroundReady().then(() => importFromString(jsonString)); - return; - } - // create objects in background context - const json = BG.tryJSONparse(jsonString) || []; - if (typeof json.slice != 'function') { - json.length = 0; - } - const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); - const oldStylesByName = json.length && new Map( - oldStyles.map(style => [style.name.trim(), style])); - - const stats = { - added: {names: [], ids: [], legend: 'importReportLegendAdded'}, - unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'}, - metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'}, - metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'}, - codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'}, - invalid: {names: [], legend: 'importReportLegendInvalid'}, - }; - - let index = 0; - let lastRenderTime = performance.now(); - const renderQueue = []; - const RENDER_NAP_TIME_MAX = 1000; // ms - const RENDER_QUEUE_MAX = 50; // number of styles - const SAVE_OPTIONS = {reason: 'import', notify: false}; - - return new Promise(proceed); - - function proceed(resolve) { - while (index < json.length) { - const item = json[index++]; - const info = analyze(item); - if (info) { - // using saveStyle directly since json was parsed in background page context - return BG.saveStyle(Object.assign(item, SAVE_OPTIONS)) - .then(style => account({style, info, resolve})); - } - } - renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); - renderQueue.length = 0; - done(resolve); - } - - function analyze(item) { - if (!item || !item.name || !item.name.trim() || typeof item != 'object' - || (item.sections && typeof item.sections.slice != 'function')) { - stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`); - return; - } - item.name = item.name.trim(); - const byId = BG.cachedStyles.byId.get(item.id); - const byName = oldStylesByName.get(item.name); - oldStylesByName.delete(item.name); - let oldStyle; - if (byId) { - if (sameStyle(byId, item)) { - oldStyle = byId; - } else { - item.id = null; - } - } - if (!oldStyle && byName) { - item.id = byName.id; - oldStyle = byName; - } - const oldStyleKeys = oldStyle && Object.keys(oldStyle); - const metaEqual = oldStyleKeys && - oldStyleKeys.length == Object.keys(item).length && - oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]); - const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item); - if (metaEqual && codeEqual) { - stats.unchanged.names.push(oldStyle.name); - stats.unchanged.ids.push(oldStyle.id); - return; - } - return {oldStyle, metaEqual, codeEqual}; - } - - function sameStyle(oldStyle, newStyle) { - return oldStyle.name.trim() === newStyle.name.trim() || - ['updateUrl', 'originalMd5', 'originalDigest'] - .some(field => oldStyle[field] && oldStyle[field] == newStyle[field]); - } - - function account({style, info, resolve}) { - renderQueue.push(style); - if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX - || renderQueue.length > RENDER_QUEUE_MAX) { - renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); - setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id)); - renderQueue.length = 0; - lastRenderTime = performance.now(); - } - setTimeout(proceed, 0, resolve); - const {oldStyle, metaEqual, codeEqual} = info; - if (!oldStyle) { - stats.added.names.push(style.name); - stats.added.ids.push(style.id); - return; - } - if (!metaEqual && !codeEqual) { - stats.metaAndCode.names.push(reportNameChange(oldStyle, style)); - stats.metaAndCode.ids.push(style.id); - return; - } - if (!codeEqual) { - stats.codeOnly.names.push(style.name); - stats.codeOnly.ids.push(style.id); - return; - } - stats.metaOnly.names.push(reportNameChange(oldStyle, style)); - stats.metaOnly.ids.push(style.id); - } - - function done(resolve) { - const numChanged = stats.metaAndCode.names.length + - stats.metaOnly.names.length + - stats.codeOnly.names.length + - stats.added.names.length; - Promise.resolve(numChanged && refreshAllTabs()).then(() => { - const report = Object.keys(stats) - .filter(kind => stats[kind].names.length) - .map(kind => { - const {ids, names, legend} = stats[kind]; - const listItemsWithId = (name, i) => - $element({dataset: {id: ids[i]}, textContent: name}); - const listItems = name => - $element({textContent: name}); - const block = - $element({tag: 'details', dataset: {id: kind}, appendChild: [ - $element({tag: 'summary', appendChild: - $element({tag: 'b', textContent: names.length + ' ' + t(legend)}) - }), - $element({tag: 'small', appendChild: - names.map(ids ? listItemsWithId : listItems) - }), - ]}); - return block; - }); - scrollTo(0, 0); - messageBox({ - title: t('importReportTitle'), - contents: report.length ? report : t('importReportUnchanged'), - buttons: [t('confirmOK'), numChanged && t('undo')], - onshow: bindClick, - }).then(({button, enter, esc}) => { - if (button == 1) { - undo(); - } - }); - resolve(numChanged); - }); - } - - function undo() { - const oldStylesById = new Map(oldStyles.map(style => [style.id, style])); - const newIds = [ - ...stats.metaAndCode.ids, - ...stats.metaOnly.ids, - ...stats.codeOnly.ids, - ...stats.added.ids, - ]; - let resolve; - index = 0; - return new Promise(resolve_ => { - resolve = resolve_; - undoNextId(); - }).then(refreshAllTabs) - .then(() => messageBox({ - title: t('importReportUndoneTitle'), - contents: newIds.length + ' ' + t('importReportUndone'), - buttons: [t('confirmOK')], - })); - function undoNextId() { - if (index == newIds.length) { - resolve(); - return; - } - const id = newIds[index++]; - deleteStyleSafe({id, notify: false}).then(id => { - const oldStyle = oldStylesById.get(id); - if (oldStyle) { - saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) - .then(undoNextId); - } else { - undoNextId(); - } - }); - } - } - - function bindClick(box) { - const highlightElement = event => { - const styleElement = $('#style-' + event.target.dataset.id); - if (styleElement) { - scrollElementIntoView(styleElement); - animateElement(styleElement); - } - }; - for (const block of $$('details')) { - if (block.dataset.id != 'invalid') { - block.style.cursor = 'pointer'; - block.onclick = highlightElement; - } - } - } - - function limitString(s, limit = 100) { - return s.length <= limit ? s : s.substr(0, limit) + '...'; - } - - function reportNameChange(oldStyle, newStyle) { - return newStyle.name != oldStyle.name - ? oldStyle.name + ' —> ' + newStyle.name - : oldStyle.name; - } - - function refreshAllTabs() { - return Promise.all([ - getActiveTab(), - getOwnTab(), - ]).then(([activeTab, ownTab]) => new Promise(resolve => { - // list all tabs including chrome-extension:// which can be ours - queryTabs().then(tabs => { - const lastTab = tabs[tabs.length - 1]; - for (const tab of tabs) { - // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF - if (FIREFOX && !tab.width) { - if (tab == lastTab) { - resolve(); - } - continue; - } - getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { - const message = {method: 'styleReplaceAll', styles}; - if (tab.id == ownTab.id) { - applyOnMessage(message); - } else { - invokeOrPostpone(tab.id == activeTab.id, - chrome.tabs.sendMessage, tab.id, message, ignoreChromeError); - } - setTimeout(BG.updateIcon, 0, tab, styles); - if (tab == lastTab) { - resolve(); - } - }); - } - }); - })); - } -} - - -$('#file-all-styles').onclick = () => { - getStylesSafe().then(styles => { - const text = JSON.stringify(styles, null, '\t'); - const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); - return url; - // for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600 - }).then(fetch) - .then(res => res.blob()) - .then(blob => { - const objectURL = URL.createObjectURL(blob); - let link = $element({ - tag:'a', - href: objectURL, - type: 'application/json', - download: generateFileName(), - }); - // TODO: remove the fallback when FF multi-process bug is fixed - if (!FIREFOX) { - link.dispatchEvent(new MouseEvent('click')); - setTimeout(() => URL.revokeObjectURL(objectURL)); - } else { - const iframe = document.body.appendChild($element({ - tag: 'iframe', - style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'), - })); - doTimeout().then(() => { - link = iframe.contentDocument.importNode(link, true); - iframe.contentDocument.body.appendChild(link); - }) - .then(doTimeout) - .then(() => link.dispatchEvent(new MouseEvent('click'))) - .then(doTimeout(1000)) - .then(() => { - URL.revokeObjectURL(objectURL); - iframe.remove(); - }); - } - }); - - function generateFileName() { - const today = new Date(); - const dd = ('0' + today.getDate()).substr(-2); - const mm = ('0' + (today.getMonth() + 1)).substr(-2); - const yyyy = today.getFullYear(); - return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`; - } -}; - - -$('#unfile-all-styles').onclick = () => { - importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); -}; - -Object.assign(document.body, { - ondragover(event) { - const hasFiles = event.dataTransfer.types.includes('Files'); - event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none'; - this.classList.toggle('dropzone', hasFiles); - if (hasFiles) { - event.preventDefault(); - clearTimeout(this.fadeoutTimer); - this.classList.remove('fadeout'); - } - }, - ondragend(event) { - animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => { - this.style.animationDuration = ''; - }); - }, - ondragleave(event) { - try { - // in Firefox event.target could be XUL browser and hence there is no permission to access it - if (event.target === this) { - this.ondragend(); - } - } catch (e) { - this.ondragend(); - } - }, - ondrop(event) { - this.ondragend(); - if (event.dataTransfer.files.length) { - event.preventDefault(); - if ($('#onlyUpdates input').checked) { - $('#onlyUpdates input').click(); - } - importFromFile({file: event.dataTransfer.files[0]}); - } - }, -}); diff --git a/apply.js b/content/apply.js similarity index 100% rename from apply.js rename to content/apply.js diff --git a/install.js b/content/install.js similarity index 96% rename from install.js rename to content/install.js index 7ce385e3..f8bba479 100644 --- a/install.js +++ b/content/install.js @@ -1,360 +1,360 @@ -'use strict'; - -const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium -const FIREFOX = /Firefox/.test(navigator.userAgent); -const VIVALDI = /Vivaldi/.test(navigator.userAgent); -const OPERA = /OPR/.test(navigator.userAgent); - -document.addEventListener('stylishUpdate', onUpdateClicked); -document.addEventListener('stylishUpdateChrome', onUpdateClicked); -document.addEventListener('stylishUpdateOpera', onUpdateClicked); - -document.addEventListener('stylishInstall', onInstallClicked); -document.addEventListener('stylishInstallChrome', onInstallClicked); -document.addEventListener('stylishInstallOpera', onInstallClicked); - -chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - // orphaned content script check - if (msg.method == 'ping') { - sendResponse(true); - } -}); - -// TODO: remove the following statement when USO is fixed -document.documentElement.appendChild(document.createElement('script')).text = '(' + - function() { - let settings; - document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { - document.removeEventListener('stylusFixBuggyUSOsettings', _); - settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search); - }); - const originalResponseJson = Response.prototype.json; - Response.prototype.json = function(...args) { - return originalResponseJson.call(this, ...args).then(json => { - Response.prototype.json = originalResponseJson; - if (!settings || typeof ((json || {}).style_settings || {}).every != 'function') { - return json; - } - const images = new Map(); - for (const jsonSetting of json.style_settings) { - let value = settings.get('ik-' + jsonSetting.install_key); - if (!value - || !jsonSetting.style_setting_options - || !jsonSetting.style_setting_options[0]) { - continue; - } - if (value.startsWith('ik-')) { - value = value.replace(/^ik-/, ''); - const defaultItem = jsonSetting.style_setting_options.find(item => item.default); - if (!defaultItem || defaultItem.install_key != value) { - if (defaultItem) { - defaultItem.default = false; - } - jsonSetting.style_setting_options.some(item => { - if (item.install_key == value) { - item.default = true; - return true; - } - }); - } - } else if (jsonSetting.setting_type == 'image') { - jsonSetting.style_setting_options.some(item => { - if (item.default) { - item.default = false; - return true; - } - }); - images.set(jsonSetting.install_key, value); - } else { - const item = jsonSetting.style_setting_options[0]; - if (item.value !== value && item.install_key == 'placeholder') { - item.value = value; - } - } - } - if (images.size) { - new MutationObserver((_, observer) => { - if (!document.getElementById('style-settings')) { - return; - } - observer.disconnect(); - for (const [name, url] of images.entries()) { - const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`); - const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url')); - if (elUrl) { - elUrl.value = url; - } - } - }).observe(document, {childList: true, subtree: true}); - } - return json; - }); - }; - } + ')()'; - -// TODO: remove the following statement when USO pagination is fixed -if (location.search.includes('category=')) { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - new MutationObserver((_, observer) => { - if (!document.getElementById('pagination')) { - return; - } - observer.disconnect(); - const category = '&' + location.search.match(/category=[^&]+/)[0]; - const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); - for (let i = 0; i < links.length; i++) { - links[i].href += category; - } - }).observe(document, {childList: true, subtree: true}); - }); -} - -new MutationObserver((mutations, observer) => { - if (document.body) { - observer.disconnect(); - // TODO: remove the following statement when USO pagination title is fixed - document.title = document.title.replace(/^\d+&category=/, ''); - chrome.runtime.sendMessage({ - method: 'getStyles', - url: getMeta('stylish-id-url') || location.href - }, checkUpdatability); - } -}).observe(document.documentElement, {childList: true}); - -/* since we are using "stylish-code-chrome" meta key on all browsers and - US.o does not provide "advanced settings" on this url if browser is not Chrome, - we need to fix this URL using "stylish-update-url" meta key -*/ -function getStyleURL() { - const url = getMeta('stylish-code-chrome'); - // TODO: remove when USO is fixed - const directUrl = getMeta('stylish-update-url'); - if (directUrl.includes('?') && !url.includes('?')) { - /* get custom settings from the update url */ - return Object.assign(new URL(url), { - search: (new URL(directUrl)).search - }).href; - } - return url; -} - -function checkUpdatability([installedStyle]) { - // TODO: remove the following statement when USO is fixed - document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { - detail: installedStyle && installedStyle.updateUrl, - })); - if (!installedStyle) { - sendEvent('styleCanBeInstalledChrome'); - return; - } - const md5Url = getMeta('stylish-md5-url'); - if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) { - getResource(md5Url).then(md5 => { - reportUpdatable(md5 != installedStyle.originalMd5); - }); - } else { - getResource(getStyleURL()).then(code => { - reportUpdatable(code === null || - !styleSectionsEqual(JSON.parse(code), installedStyle)); - }); - } - - function reportUpdatable(isUpdatable) { - sendEvent( - isUpdatable - ? 'styleCanBeUpdatedChrome' - : 'styleAlreadyInstalledChrome', - { - updateUrl: installedStyle.updateUrl - } - ); - } -} - - -function sendEvent(type, detail = null) { - if (FIREFOX) { - type = type.replace('Chrome', ''); - } else if (OPERA || VIVALDI) { - type = type.replace('Chrome', 'Opera'); - } - detail = {detail}; - if (typeof cloneInto != 'undefined') { - // Firefox requires explicit cloning, however USO can't process our messages anyway - // because USO tries to use a global "event" variable deprecated in Firefox - detail = cloneInto(detail, document); // eslint-disable-line no-undef - } - onDOMready().then(() => { - document.dispatchEvent(new CustomEvent(type, detail)); - }); -} - - -function onInstallClicked() { - if (!orphanCheck || !orphanCheck()) { - return; - } - getResource(getMeta('stylish-description')) - .then(name => saveStyleCode('styleInstall', name)) - .then(() => getResource(getMeta('stylish-install-ping-url-chrome'))); -} - - -function onUpdateClicked() { - if (!orphanCheck || !orphanCheck()) { - return; - } - chrome.runtime.sendMessage({ - method: 'getStyles', - url: getMeta('stylish-id-url') || location.href, - }, ([style]) => { - saveStyleCode('styleUpdate', style.name, {id: style.id}); - }); -} - - -function saveStyleCode(message, name, addProps) { - return new Promise(resolve => { - if (!confirm(chrome.i18n.getMessage(message, [name]))) { - return; - } - enableUpdateButton(false); - getResource(getStyleURL()).then(code => { - chrome.runtime.sendMessage( - Object.assign(JSON.parse(code), addProps, { - method: 'saveStyle', - reason: 'update', - }), - style => { - if (message == 'styleUpdate' && style.updateUrl.includes('?')) { - enableUpdateButton(true); - } else { - sendEvent('styleInstalledChrome'); - } - } - ); - resolve(); - }); - }); - - function enableUpdateButton(state) { - const button = document.getElementById('update_style_button'); - if (button) { - button.style.cssText = state ? '' : - 'pointer-events: none !important; opacity: .25 !important;'; - } - } -} - - -function getMeta(name) { - const e = document.querySelector(`link[rel="${name}"]`); - return e ? e.getAttribute('href') : null; -} - - -function getResource(url) { - return new Promise(resolve => { - if (url.startsWith('#')) { - resolve(document.getElementById(url.slice(1)).textContent); - } else { - chrome.runtime.sendMessage({method: 'download', url}, resolve); - } - }); -} - - -function styleSectionsEqual({sections: a}, {sections: b}) { - if (!a || !b) { - return undefined; - } - if (a.length != b.length) { - return false; - } - const checkedInB = []; - return a.every(sectionA => b.some(sectionB => { - if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) { - checkedInB.push(sectionB); - return true; - } - })); - - function propertiesEqual(secA, secB) { - for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { - if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { - return false; - } - } - return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b); - } - - function equalOrEmpty(a, b, telltale, comparator) { - const typeA = a && typeof a[telltale] == 'function'; - const typeB = b && typeof b[telltale] == 'function'; - return ( - (a === null || a === undefined || (typeA && !a.length)) && - (b === null || b === undefined || (typeB && !b.length)) - ) || typeA && typeB && a.length == b.length && comparator(a, b); - } - - function arrayMirrors(array1, array2) { - for (const el of array1) { - if (array2.indexOf(el) < 0) { - return false; - } - } - for (const el of array2) { - if (array1.indexOf(el) < 0) { - return false; - } - } - return true; - } -} - - -function onDOMready() { - if (document.readyState != 'loading') { - return Promise.resolve(); - } - return new Promise(resolve => { - document.addEventListener('DOMContentLoaded', function _() { - document.removeEventListener('DOMContentLoaded', _); - resolve(); - }); - }); -} - - -function orphanCheck() { - const port = chrome.runtime.connect(); - if (port) { - port.disconnect(); - return true; - } - // we're orphaned due to an extension update - // we can detach event listeners - document.removeEventListener('stylishUpdate', onUpdateClicked); - document.removeEventListener('stylishUpdateChrome', onUpdateClicked); - document.removeEventListener('stylishUpdateOpera', onUpdateClicked); - - document.removeEventListener('stylishInstall', onInstallClicked); - document.removeEventListener('stylishInstallChrome', onInstallClicked); - document.removeEventListener('stylishInstallOpera', onInstallClicked); - - // 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 - [ - 'checkUpdatability', - 'getMeta', - 'getResource', - 'onDOMready', - 'onInstallClicked', - 'onUpdateClicked', - 'orphanCheck', - 'saveStyleCode', - 'sendEvent', - 'styleSectionsEqual', - ].forEach(fn => (window[fn] = null)); -} +'use strict'; + +const CHROMIUM = /Chromium/.test(navigator.userAgent); // non-Windows Chromium +const FIREFOX = /Firefox/.test(navigator.userAgent); +const VIVALDI = /Vivaldi/.test(navigator.userAgent); +const OPERA = /OPR/.test(navigator.userAgent); + +document.addEventListener('stylishUpdate', onUpdateClicked); +document.addEventListener('stylishUpdateChrome', onUpdateClicked); +document.addEventListener('stylishUpdateOpera', onUpdateClicked); + +document.addEventListener('stylishInstall', onInstallClicked); +document.addEventListener('stylishInstallChrome', onInstallClicked); +document.addEventListener('stylishInstallOpera', onInstallClicked); + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + // orphaned content script check + if (msg.method == 'ping') { + sendResponse(true); + } +}); + +// TODO: remove the following statement when USO is fixed +document.documentElement.appendChild(document.createElement('script')).text = '(' + + function() { + let settings; + document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { + document.removeEventListener('stylusFixBuggyUSOsettings', _); + settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search); + }); + const originalResponseJson = Response.prototype.json; + Response.prototype.json = function(...args) { + return originalResponseJson.call(this, ...args).then(json => { + Response.prototype.json = originalResponseJson; + if (!settings || typeof ((json || {}).style_settings || {}).every != 'function') { + return json; + } + const images = new Map(); + for (const jsonSetting of json.style_settings) { + let value = settings.get('ik-' + jsonSetting.install_key); + if (!value + || !jsonSetting.style_setting_options + || !jsonSetting.style_setting_options[0]) { + continue; + } + if (value.startsWith('ik-')) { + value = value.replace(/^ik-/, ''); + const defaultItem = jsonSetting.style_setting_options.find(item => item.default); + if (!defaultItem || defaultItem.install_key != value) { + if (defaultItem) { + defaultItem.default = false; + } + jsonSetting.style_setting_options.some(item => { + if (item.install_key == value) { + item.default = true; + return true; + } + }); + } + } else if (jsonSetting.setting_type == 'image') { + jsonSetting.style_setting_options.some(item => { + if (item.default) { + item.default = false; + return true; + } + }); + images.set(jsonSetting.install_key, value); + } else { + const item = jsonSetting.style_setting_options[0]; + if (item.value !== value && item.install_key == 'placeholder') { + item.value = value; + } + } + } + if (images.size) { + new MutationObserver((_, observer) => { + if (!document.getElementById('style-settings')) { + return; + } + observer.disconnect(); + for (const [name, url] of images.entries()) { + const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`); + const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url')); + if (elUrl) { + elUrl.value = url; + } + } + }).observe(document, {childList: true, subtree: true}); + } + return json; + }); + }; + } + ')()'; + +// TODO: remove the following statement when USO pagination is fixed +if (location.search.includes('category=')) { + document.addEventListener('DOMContentLoaded', function _() { + document.removeEventListener('DOMContentLoaded', _); + new MutationObserver((_, observer) => { + if (!document.getElementById('pagination')) { + return; + } + observer.disconnect(); + const category = '&' + location.search.match(/category=[^&]+/)[0]; + const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); + for (let i = 0; i < links.length; i++) { + links[i].href += category; + } + }).observe(document, {childList: true, subtree: true}); + }); +} + +new MutationObserver((mutations, observer) => { + if (document.body) { + observer.disconnect(); + // TODO: remove the following statement when USO pagination title is fixed + document.title = document.title.replace(/^\d+&category=/, ''); + chrome.runtime.sendMessage({ + method: 'getStyles', + url: getMeta('stylish-id-url') || location.href + }, checkUpdatability); + } +}).observe(document.documentElement, {childList: true}); + +/* since we are using "stylish-code-chrome" meta key on all browsers and + US.o does not provide "advanced settings" on this url if browser is not Chrome, + we need to fix this URL using "stylish-update-url" meta key +*/ +function getStyleURL() { + const url = getMeta('stylish-code-chrome'); + // TODO: remove when USO is fixed + const directUrl = getMeta('stylish-update-url'); + if (directUrl.includes('?') && !url.includes('?')) { + /* get custom settings from the update url */ + return Object.assign(new URL(url), { + search: (new URL(directUrl)).search + }).href; + } + return url; +} + +function checkUpdatability([installedStyle]) { + // TODO: remove the following statement when USO is fixed + document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', { + detail: installedStyle && installedStyle.updateUrl, + })); + if (!installedStyle) { + sendEvent('styleCanBeInstalledChrome'); + return; + } + const md5Url = getMeta('stylish-md5-url'); + if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) { + getResource(md5Url).then(md5 => { + reportUpdatable(md5 != installedStyle.originalMd5); + }); + } else { + getResource(getStyleURL()).then(code => { + reportUpdatable(code === null || + !styleSectionsEqual(JSON.parse(code), installedStyle)); + }); + } + + function reportUpdatable(isUpdatable) { + sendEvent( + isUpdatable + ? 'styleCanBeUpdatedChrome' + : 'styleAlreadyInstalledChrome', + { + updateUrl: installedStyle.updateUrl + } + ); + } +} + + +function sendEvent(type, detail = null) { + if (FIREFOX) { + type = type.replace('Chrome', ''); + } else if (OPERA || VIVALDI) { + type = type.replace('Chrome', 'Opera'); + } + detail = {detail}; + if (typeof cloneInto != 'undefined') { + // Firefox requires explicit cloning, however USO can't process our messages anyway + // because USO tries to use a global "event" variable deprecated in Firefox + detail = cloneInto(detail, document); // eslint-disable-line no-undef + } + onDOMready().then(() => { + document.dispatchEvent(new CustomEvent(type, detail)); + }); +} + + +function onInstallClicked() { + if (!orphanCheck || !orphanCheck()) { + return; + } + getResource(getMeta('stylish-description')) + .then(name => saveStyleCode('styleInstall', name)) + .then(() => getResource(getMeta('stylish-install-ping-url-chrome'))); +} + + +function onUpdateClicked() { + if (!orphanCheck || !orphanCheck()) { + return; + } + chrome.runtime.sendMessage({ + method: 'getStyles', + url: getMeta('stylish-id-url') || location.href, + }, ([style]) => { + saveStyleCode('styleUpdate', style.name, {id: style.id}); + }); +} + + +function saveStyleCode(message, name, addProps) { + return new Promise(resolve => { + if (!confirm(chrome.i18n.getMessage(message, [name]))) { + return; + } + enableUpdateButton(false); + getResource(getStyleURL()).then(code => { + chrome.runtime.sendMessage( + Object.assign(JSON.parse(code), addProps, { + method: 'saveStyle', + reason: 'update', + }), + style => { + if (message == 'styleUpdate' && style.updateUrl.includes('?')) { + enableUpdateButton(true); + } else { + sendEvent('styleInstalledChrome'); + } + } + ); + resolve(); + }); + }); + + function enableUpdateButton(state) { + const button = document.getElementById('update_style_button'); + if (button) { + button.style.cssText = state ? '' : + 'pointer-events: none !important; opacity: .25 !important;'; + } + } +} + + +function getMeta(name) { + const e = document.querySelector(`link[rel="${name}"]`); + return e ? e.getAttribute('href') : null; +} + + +function getResource(url) { + return new Promise(resolve => { + if (url.startsWith('#')) { + resolve(document.getElementById(url.slice(1)).textContent); + } else { + chrome.runtime.sendMessage({method: 'download', url}, resolve); + } + }); +} + + +function styleSectionsEqual({sections: a}, {sections: b}) { + if (!a || !b) { + return undefined; + } + if (a.length != b.length) { + return false; + } + const checkedInB = []; + return a.every(sectionA => b.some(sectionB => { + if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) { + checkedInB.push(sectionB); + return true; + } + })); + + function propertiesEqual(secA, secB) { + for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { + if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { + return false; + } + } + return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b); + } + + function equalOrEmpty(a, b, telltale, comparator) { + const typeA = a && typeof a[telltale] == 'function'; + const typeB = b && typeof b[telltale] == 'function'; + return ( + (a === null || a === undefined || (typeA && !a.length)) && + (b === null || b === undefined || (typeB && !b.length)) + ) || typeA && typeB && a.length == b.length && comparator(a, b); + } + + function arrayMirrors(array1, array2) { + for (const el of array1) { + if (array2.indexOf(el) < 0) { + return false; + } + } + for (const el of array2) { + if (array1.indexOf(el) < 0) { + return false; + } + } + return true; + } +} + + +function onDOMready() { + if (document.readyState != 'loading') { + return Promise.resolve(); + } + return new Promise(resolve => { + document.addEventListener('DOMContentLoaded', function _() { + document.removeEventListener('DOMContentLoaded', _); + resolve(); + }); + }); +} + + +function orphanCheck() { + const port = chrome.runtime.connect(); + if (port) { + port.disconnect(); + return true; + } + // we're orphaned due to an extension update + // we can detach event listeners + document.removeEventListener('stylishUpdate', onUpdateClicked); + document.removeEventListener('stylishUpdateChrome', onUpdateClicked); + document.removeEventListener('stylishUpdateOpera', onUpdateClicked); + + document.removeEventListener('stylishInstall', onInstallClicked); + document.removeEventListener('stylishInstallChrome', onInstallClicked); + document.removeEventListener('stylishInstallOpera', onInstallClicked); + + // 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 + [ + 'checkUpdatability', + 'getMeta', + 'getResource', + 'onDOMready', + 'onInstallClicked', + 'onUpdateClicked', + 'orphanCheck', + 'saveStyleCode', + 'sendEvent', + 'styleSectionsEqual', + ].forEach(fn => (window[fn] = null)); +} diff --git a/edit.js b/edit/edit.js similarity index 100% rename from edit.js rename to edit/edit.js diff --git a/options/index.html b/index.html similarity index 100% rename from options/index.html rename to index.html diff --git a/dom.js b/js/dom.js similarity index 100% rename from dom.js rename to js/dom.js diff --git a/localization.js b/js/localization.js similarity index 100% rename from localization.js rename to js/localization.js diff --git a/prefs.js b/js/prefs.js similarity index 100% rename from prefs.js rename to js/prefs.js diff --git a/manage/fileSaveLoad.js b/manage/fileSaveLoad.js new file mode 100644 index 00000000..125ed9e0 --- /dev/null +++ b/manage/fileSaveLoad.js @@ -0,0 +1,791 @@ +/* global messageBox, handleUpdate, applyOnMessage */ +'use strict'; + +const STYLISH_DUMP_FILE_EXT = '.txt'; +const STYLUS_BACKUP_FILE_EXT = '.json'; + + +function importFromFile({fileTypeFilter, file} = {}) { + return new Promise(resolve => { + const fileInput = document.createElement('input'); + if (file) { + readFile(); + return; + } + fileInput.style.display = 'none'; + fileInput.type = 'file'; + fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT; + fileInput.acceptCharset = 'utf-8'; + + document.body.appendChild(fileInput); + fileInput.initialValue = fileInput.value; + fileInput.onchange = readFile; + fileInput.click(); + + function readFile() { + if (file || fileInput.value !== fileInput.initialValue) { + file = file || fileInput.files[0]; + if (file.size > 100e6) { + console.warn("100MB backup? I don't believe you."); + importFromString('').then(resolve); + return; + } + document.body.style.cursor = 'wait'; + const fReader = new FileReader(); + fReader.onloadend = event => { + fileInput.remove(); + importFromString(event.target.result).then(numStyles => { + document.body.style.cursor = ''; + resolve(numStyles); + }); + }; + fReader.readAsText(file, 'utf-8'); + } + } + }); +} + + +function importFromString(jsonString) { + if (!BG) { + onBackgroundReady().then(() => importFromString(jsonString)); + return; + } + // create objects in background context + const json = BG.tryJSONparse(jsonString) || []; + if (typeof json.slice != 'function') { + json.length = 0; + } + const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); + const oldStylesByName = json.length && new Map( + oldStyles.map(style => [style.name.trim(), style])); + + const stats = { + added: {names: [], ids: [], legend: 'importReportLegendAdded'}, + unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'}, + metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'}, + metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'}, + codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'}, + invalid: {names: [], legend: 'importReportLegendInvalid'}, + }; + + let index = 0; + let lastRenderTime = performance.now(); + const renderQueue = []; + const RENDER_NAP_TIME_MAX = 1000; // ms + const RENDER_QUEUE_MAX = 50; // number of styles + const SAVE_OPTIONS = {reason: 'import', notify: false}; + + return new Promise(proceed); + + function proceed(resolve) { + while (index < json.length) { + const item = json[index++]; + const info = analyze(item); + if (info) { + // using saveStyle directly since json was parsed in background page context + return BG.saveStyle(Object.assign(item, SAVE_OPTIONS)) + .then(style => account({style, info, resolve})); + } + } + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + renderQueue.length = 0; + done(resolve); + } + + function analyze(item) { + if (!item || !item.name || !item.name.trim() || typeof item != 'object' + || (item.sections && typeof item.sections.slice != 'function')) { + stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`); + return; + } + item.name = item.name.trim(); + const byId = BG.cachedStyles.byId.get(item.id); + const byName = oldStylesByName.get(item.name); + oldStylesByName.delete(item.name); + let oldStyle; + if (byId) { + if (sameStyle(byId, item)) { + oldStyle = byId; + } else { + item.id = null; + } + } + if (!oldStyle && byName) { + item.id = byName.id; + oldStyle = byName; + } + const oldStyleKeys = oldStyle && Object.keys(oldStyle); + const metaEqual = oldStyleKeys && + oldStyleKeys.length == Object.keys(item).length && + oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]); + const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item); + if (metaEqual && codeEqual) { + stats.unchanged.names.push(oldStyle.name); + stats.unchanged.ids.push(oldStyle.id); + return; + } + return {oldStyle, metaEqual, codeEqual}; + } + + function sameStyle(oldStyle, newStyle) { + return oldStyle.name.trim() === newStyle.name.trim() || + ['updateUrl', 'originalMd5', 'originalDigest'] + .some(field => oldStyle[field] && oldStyle[field] == newStyle[field]); + } + + function account({style, info, resolve}) { + renderQueue.push(style); + if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX + || renderQueue.length > RENDER_QUEUE_MAX) { + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id)); + renderQueue.length = 0; + lastRenderTime = performance.now(); + } + setTimeout(proceed, 0, resolve); + const {oldStyle, metaEqual, codeEqual} = info; + if (!oldStyle) { + stats.added.names.push(style.name); + stats.added.ids.push(style.id); + return; + } + if (!metaEqual && !codeEqual) { + stats.metaAndCode.names.push(reportNameChange(oldStyle, style)); + stats.metaAndCode.ids.push(style.id); + return; + } + if (!codeEqual) { + stats.codeOnly.names.push(style.name); + stats.codeOnly.ids.push(style.id); + return; + } + stats.metaOnly.names.push(reportNameChange(oldStyle, style)); + stats.metaOnly.ids.push(style.id); + } + + function done(resolve) { + const numChanged = stats.metaAndCode.names.length + + stats.metaOnly.names.length + + stats.codeOnly.names.length + + stats.added.names.length; + Promise.resolve(numChanged && refreshAllTabs()).then(() => { + const report = Object.keys(stats) + .filter(kind => stats[kind].names.length) + .map(kind => { + const {ids, names, legend} = stats[kind]; + const listItemsWithId = (name, i) => + $element({dataset: {id: ids[i]}, textContent: name}); + const listItems = name => + $element({textContent: name}); + const block = + $element({tag: 'details', dataset: {id: kind}, appendChild: [ + $element({tag: 'summary', appendChild: + $element({tag: 'b', textContent: names.length + ' ' + t(legend)}) + }), + $element({tag: 'small', appendChild: + names.map(ids ? listItemsWithId : listItems) + }), + ]}); + return block; + }); + scrollTo(0, 0); + messageBox({ + title: t('importReportTitle'), + contents: report.length ? report : t('importReportUnchanged'), + buttons: [t('confirmOK'), numChanged && t('undo')], + onshow: bindClick, + }).then(({button, enter, esc}) => { + if (button == 1) { + undo(); + } + }); + resolve(numChanged); + }); + } + + function undo() { + const oldStylesById = new Map(oldStyles.map(style => [style.id, style])); + const newIds = [ + ...stats.metaAndCode.ids, + ...stats.metaOnly.ids, + ...stats.codeOnly.ids, + ...stats.added.ids, + ]; + let resolve; + index = 0; + return new Promise(resolve_ => { + resolve = resolve_; + undoNextId(); + }).then(refreshAllTabs) + .then(() => messageBox({ + title: t('importReportUndoneTitle'), + contents: newIds.length + ' ' + t('importReportUndone'), + buttons: [t('confirmOK')], + })); + function undoNextId() { + if (index == newIds.length) { + resolve(); + return; + } + const id = newIds[index++]; + deleteStyleSafe({id, notify: false}).then(id => { + const oldStyle = oldStylesById.get(id); + if (oldStyle) { + saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) + .then(undoNextId); + } else { + undoNextId(); + } + }); + } + } + + function bindClick(box) { + const highlightElement = event => { + const styleElement = $('#style-' + event.target.dataset.id); + if (styleElement) { + scrollElementIntoView(styleElement); + animateElement(styleElement); + } + }; + for (const block of $$('details')) { + if (block.dataset.id != 'invalid') { + block.style.cursor = 'pointer'; + block.onclick = highlightElement; + } + } + } + + function limitString(s, limit = 100) { + return s.length <= limit ? s : s.substr(0, limit) + '...'; + } + + function reportNameChange(oldStyle, newStyle) { + return newStyle.name != oldStyle.name + ? oldStyle.name + ' —> ' + newStyle.name + : oldStyle.name; + } + + function refreshAllTabs() { + return Promise.all([ + getActiveTab(), + getOwnTab(), + ]).then(([activeTab, ownTab]) => new Promise(resolve => { + // list all tabs including chrome-extension:// which can be ours + queryTabs().then(tabs => { + const lastTab = tabs[tabs.length - 1]; + for (const tab of tabs) { + // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF + if (FIREFOX && !tab.width) { + if (tab == lastTab) { + resolve(); + } + continue; + } + getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { + const message = {method: 'styleReplaceAll', styles}; + if (tab.id == ownTab.id) { + applyOnMessage(message); + } else { + invokeOrPostpone(tab.id == activeTab.id, + chrome.tabs.sendMessage, tab.id, message, ignoreChromeError); + } + setTimeout(BG.updateIcon, 0, tab, styles); + if (tab == lastTab) { + resolve(); + } + }); + } + }); + })); + } +} + + +$('#file-all-styles').onclick = () => { + getStylesSafe().then(styles => { + const text = JSON.stringify(styles, null, '\t'); + const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); + return url; + // for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600 + }).then(fetch) + .then(res => res.blob()) + .then(blob => { + const objectURL = URL.createObjectURL(blob); + let link = $element({ + tag:'a', + href: objectURL, + type: 'application/json', + download: generateFileName(), + }); + // TODO: remove the fallback when FF multi-process bug is fixed + if (!FIREFOX) { + link.dispatchEvent(new MouseEvent('click')); + setTimeout(() => URL.revokeObjectURL(objectURL)); + } else { + const iframe = document.body.appendChild($element({ + tag: 'iframe', + style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'), + })); + doTimeout().then(() => { + link = iframe.contentDocument.importNode(link, true); + iframe.contentDocument.body.appendChild(link); + }) + .then(doTimeout) + .then(() => link.dispatchEvent(new MouseEvent('click'))) + .then(doTimeout(1000)) + .then(() => { + URL.revokeObjectURL(objectURL); + iframe.remove(); + }); + } + }); + + function generateFileName() { + const today = new Date(); + const dd = ('0' + today.getDate()).substr(-2); + const mm = ('0' + (today.getMonth() + 1)).substr(-2); + const yyyy = today.getFullYear(); + return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`; + } +}; + + +$('#unfile-all-styles').onclick = () => { + importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); +}; + +Object.assign(document.body, { + ondragover(event) { + const hasFiles = event.dataTransfer.types.includes('Files'); + event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none'; + this.classList.toggle('dropzone', hasFiles); + if (hasFiles) { + event.preventDefault(); + clearTimeout(this.fadeoutTimer); + this.classList.remove('fadeout'); + } + }, + ondragend(event) { + animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => { + this.style.animationDuration = ''; + }); + }, + ondragleave(event) { + try { + // in Firefox event.target could be XUL browser and hence there is no permission to access it + if (event.target === this) { + this.ondragend(); + } + } catch (e) { + this.ondragend(); + } + }, + ondrop(event) { + this.ondragend(); + if (event.dataTransfer.files.length) { + event.preventDefault(); + if ($('#onlyUpdates input').checked) { + $('#onlyUpdates input').click(); + } + importFromFile({file: event.dataTransfer.files[0]}); + } + }, +}); +======= +/* global messageBox, handleUpdate, applyOnMessage */ +'use strict'; + +const STYLISH_DUMP_FILE_EXT = '.txt'; +const STYLUS_BACKUP_FILE_EXT = '.json'; + + +function importFromFile({fileTypeFilter, file} = {}) { + return new Promise(resolve => { + const fileInput = document.createElement('input'); + if (file) { + readFile(); + return; + } + fileInput.style.display = 'none'; + fileInput.type = 'file'; + fileInput.accept = fileTypeFilter || STYLISH_DUMP_FILE_EXT; + fileInput.acceptCharset = 'utf-8'; + + document.body.appendChild(fileInput); + fileInput.initialValue = fileInput.value; + fileInput.onchange = readFile; + fileInput.click(); + + function readFile() { + if (file || fileInput.value !== fileInput.initialValue) { + file = file || fileInput.files[0]; + if (file.size > 100e6) { + console.warn("100MB backup? I don't believe you."); + importFromString('').then(resolve); + return; + } + document.body.style.cursor = 'wait'; + const fReader = new FileReader(); + fReader.onloadend = event => { + fileInput.remove(); + importFromString(event.target.result).then(numStyles => { + document.body.style.cursor = ''; + resolve(numStyles); + }); + }; + fReader.readAsText(file, 'utf-8'); + } + } + }); +} + + +function importFromString(jsonString) { + if (!BG) { + onBackgroundReady().then(() => importFromString(jsonString)); + return; + } + // create objects in background context + const json = BG.tryJSONparse(jsonString) || []; + if (typeof json.slice != 'function') { + json.length = 0; + } + const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []); + const oldStylesByName = json.length && new Map( + oldStyles.map(style => [style.name.trim(), style])); + + const stats = { + added: {names: [], ids: [], legend: 'importReportLegendAdded'}, + unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'}, + metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'}, + metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'}, + codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'}, + invalid: {names: [], legend: 'importReportLegendInvalid'}, + }; + + let index = 0; + let lastRenderTime = performance.now(); + const renderQueue = []; + const RENDER_NAP_TIME_MAX = 1000; // ms + const RENDER_QUEUE_MAX = 50; // number of styles + const SAVE_OPTIONS = {reason: 'import', notify: false}; + + return new Promise(proceed); + + function proceed(resolve) { + while (index < json.length) { + const item = json[index++]; + const info = analyze(item); + if (info) { + // using saveStyle directly since json was parsed in background page context + return BG.saveStyle(Object.assign(item, SAVE_OPTIONS)) + .then(style => account({style, info, resolve})); + } + } + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + renderQueue.length = 0; + done(resolve); + } + + function analyze(item) { + if (!item || !item.name || !item.name.trim() || typeof item != 'object' + || (item.sections && typeof item.sections.slice != 'function')) { + stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`); + return; + } + item.name = item.name.trim(); + const byId = BG.cachedStyles.byId.get(item.id); + const byName = oldStylesByName.get(item.name); + oldStylesByName.delete(item.name); + let oldStyle; + if (byId) { + if (sameStyle(byId, item)) { + oldStyle = byId; + } else { + item.id = null; + } + } + if (!oldStyle && byName) { + item.id = byName.id; + oldStyle = byName; + } + const oldStyleKeys = oldStyle && Object.keys(oldStyle); + const metaEqual = oldStyleKeys && + oldStyleKeys.length == Object.keys(item).length && + oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]); + const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item); + if (metaEqual && codeEqual) { + stats.unchanged.names.push(oldStyle.name); + stats.unchanged.ids.push(oldStyle.id); + return; + } + return {oldStyle, metaEqual, codeEqual}; + } + + function sameStyle(oldStyle, newStyle) { + return oldStyle.name.trim() === newStyle.name.trim() || + ['updateUrl', 'originalMd5', 'originalDigest'] + .some(field => oldStyle[field] && oldStyle[field] == newStyle[field]); + } + + function account({style, info, resolve}) { + renderQueue.push(style); + if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX + || renderQueue.length > RENDER_QUEUE_MAX) { + renderQueue.forEach(style => handleUpdate(style, {reason: 'import'})); + setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id)); + renderQueue.length = 0; + lastRenderTime = performance.now(); + } + setTimeout(proceed, 0, resolve); + const {oldStyle, metaEqual, codeEqual} = info; + if (!oldStyle) { + stats.added.names.push(style.name); + stats.added.ids.push(style.id); + return; + } + if (!metaEqual && !codeEqual) { + stats.metaAndCode.names.push(reportNameChange(oldStyle, style)); + stats.metaAndCode.ids.push(style.id); + return; + } + if (!codeEqual) { + stats.codeOnly.names.push(style.name); + stats.codeOnly.ids.push(style.id); + return; + } + stats.metaOnly.names.push(reportNameChange(oldStyle, style)); + stats.metaOnly.ids.push(style.id); + } + + function done(resolve) { + const numChanged = stats.metaAndCode.names.length + + stats.metaOnly.names.length + + stats.codeOnly.names.length + + stats.added.names.length; + Promise.resolve(numChanged && refreshAllTabs()).then(() => { + const report = Object.keys(stats) + .filter(kind => stats[kind].names.length) + .map(kind => { + const {ids, names, legend} = stats[kind]; + const listItemsWithId = (name, i) => + $element({dataset: {id: ids[i]}, textContent: name}); + const listItems = name => + $element({textContent: name}); + const block = + $element({tag: 'details', dataset: {id: kind}, appendChild: [ + $element({tag: 'summary', appendChild: + $element({tag: 'b', textContent: names.length + ' ' + t(legend)}) + }), + $element({tag: 'small', appendChild: + names.map(ids ? listItemsWithId : listItems) + }), + ]}); + return block; + }); + scrollTo(0, 0); + messageBox({ + title: t('importReportTitle'), + contents: report.length ? report : t('importReportUnchanged'), + buttons: [t('confirmOK'), numChanged && t('undo')], + onshow: bindClick, + }).then(({button, enter, esc}) => { + if (button == 1) { + undo(); + } + }); + resolve(numChanged); + }); + } + + function undo() { + const oldStylesById = new Map(oldStyles.map(style => [style.id, style])); + const newIds = [ + ...stats.metaAndCode.ids, + ...stats.metaOnly.ids, + ...stats.codeOnly.ids, + ...stats.added.ids, + ]; + let resolve; + index = 0; + return new Promise(resolve_ => { + resolve = resolve_; + undoNextId(); + }).then(refreshAllTabs) + .then(() => messageBox({ + title: t('importReportUndoneTitle'), + contents: newIds.length + ' ' + t('importReportUndone'), + buttons: [t('confirmOK')], + })); + function undoNextId() { + if (index == newIds.length) { + resolve(); + return; + } + const id = newIds[index++]; + deleteStyleSafe({id, notify: false}).then(id => { + const oldStyle = oldStylesById.get(id); + if (oldStyle) { + saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) + .then(undoNextId); + } else { + undoNextId(); + } + }); + } + } + + function bindClick(box) { + const highlightElement = event => { + const styleElement = $('#style-' + event.target.dataset.id); + if (styleElement) { + scrollElementIntoView(styleElement); + animateElement(styleElement); + } + }; + for (const block of $$('details')) { + if (block.dataset.id != 'invalid') { + block.style.cursor = 'pointer'; + block.onclick = highlightElement; + } + } + } + + function limitString(s, limit = 100) { + return s.length <= limit ? s : s.substr(0, limit) + '...'; + } + + function reportNameChange(oldStyle, newStyle) { + return newStyle.name != oldStyle.name + ? oldStyle.name + ' —> ' + newStyle.name + : oldStyle.name; + } + + function refreshAllTabs() { + return Promise.all([ + getActiveTab(), + getOwnTab(), + ]).then(([activeTab, ownTab]) => new Promise(resolve => { + // list all tabs including chrome-extension:// which can be ours + queryTabs().then(tabs => { + const lastTab = tabs[tabs.length - 1]; + for (const tab of tabs) { + // skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF + if (FIREFOX && !tab.width) { + if (tab == lastTab) { + resolve(); + } + continue; + } + getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => { + const message = {method: 'styleReplaceAll', styles}; + if (tab.id == ownTab.id) { + applyOnMessage(message); + } else { + invokeOrPostpone(tab.id == activeTab.id, + chrome.tabs.sendMessage, tab.id, message, ignoreChromeError); + } + setTimeout(BG.updateIcon, 0, tab, styles); + if (tab == lastTab) { + resolve(); + } + }); + } + }); + })); + } +} + + +$('#file-all-styles').onclick = () => { + getStylesSafe().then(styles => { + const text = JSON.stringify(styles, null, '\t'); + const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text); + return url; + // for long URLs; https://github.com/schomery/stylus/issues/13#issuecomment-284582600 + }).then(fetch) + .then(res => res.blob()) + .then(blob => { + const objectURL = URL.createObjectURL(blob); + let link = $element({ + tag:'a', + href: objectURL, + type: 'application/json', + download: generateFileName(), + }); + // TODO: remove the fallback when FF multi-process bug is fixed + if (!FIREFOX) { + link.dispatchEvent(new MouseEvent('click')); + setTimeout(() => URL.revokeObjectURL(objectURL)); + } else { + const iframe = document.body.appendChild($element({ + tag: 'iframe', + style: 'width: 0; height: 0; position: fixed; opacity: 0;'.replace(/;/g, '!important;'), + })); + doTimeout().then(() => { + link = iframe.contentDocument.importNode(link, true); + iframe.contentDocument.body.appendChild(link); + }) + .then(doTimeout) + .then(() => link.dispatchEvent(new MouseEvent('click'))) + .then(doTimeout(1000)) + .then(() => { + URL.revokeObjectURL(objectURL); + iframe.remove(); + }); + } + }); + + function generateFileName() { + const today = new Date(); + const dd = ('0' + today.getDate()).substr(-2); + const mm = ('0' + (today.getMonth() + 1)).substr(-2); + const yyyy = today.getFullYear(); + return `stylus-${mm}-${dd}-${yyyy}${STYLUS_BACKUP_FILE_EXT}`; + } +}; + + +$('#unfile-all-styles').onclick = () => { + importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT}); +}; + +Object.assign(document.body, { + ondragover(event) { + const hasFiles = event.dataTransfer.types.includes('Files'); + event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none'; + this.classList.toggle('dropzone', hasFiles); + if (hasFiles) { + event.preventDefault(); + clearTimeout(this.fadeoutTimer); + this.classList.remove('fadeout'); + } + }, + ondragend(event) { + animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => { + this.style.animationDuration = ''; + }); + }, + ondragleave(event) { + try { + // in Firefox event.target could be XUL browser and hence there is no permission to access it + if (event.target === this) { + this.ondragend(); + } + } catch (e) { + this.ondragend(); + } + }, + ondrop(event) { + this.ondragend(); + if (event.dataTransfer.files.length) { + event.preventDefault(); + if ($('#onlyUpdates input').checked) { + $('#onlyUpdates input').click(); + } + importFromFile({file: event.dataTransfer.files[0]}); + } + }, +}); diff --git a/manage.css b/manage/manage.css similarity index 100% rename from manage.css rename to manage/manage.css diff --git a/manage.js b/manage/manage.js similarity index 100% rename from manage.js rename to manage/manage.js diff --git a/popup.css b/popup/popup.css similarity index 100% rename from popup.css rename to popup/popup.css diff --git a/popup.js b/popup/popup.js similarity index 100% rename from popup.js rename to popup/popup.js diff --git a/pull_locales.rb b/tools/pull_locales.rb similarity index 100% rename from pull_locales.rb rename to tools/pull_locales.rb diff --git a/pull_locales.sh b/tools/pull_locales.sh old mode 100755 new mode 100644 similarity index 100% rename from pull_locales.sh rename to tools/pull_locales.sh diff --git a/pull_locales_postprocess.py b/tools/pull_locales_postprocess.py similarity index 100% rename from pull_locales_postprocess.py rename to tools/pull_locales_postprocess.py diff --git a/beautify/beautify-css-mod.js b/vendor-overwrites/beautify/beautify-css-mod.js similarity index 100% rename from beautify/beautify-css-mod.js rename to vendor-overwrites/beautify/beautify-css-mod.js diff --git a/beautify/beautify-css.js b/vendor-overwrites/beautify/beautify-css.js similarity index 100% rename from beautify/beautify-css.js rename to vendor-overwrites/beautify/beautify-css.js diff --git a/codemirror-overwrites/addon/lint/css-lint.js b/vendor-overwrites/codemirror/addon/lint/css-lint.js similarity index 100% rename from codemirror-overwrites/addon/lint/css-lint.js rename to vendor-overwrites/codemirror/addon/lint/css-lint.js diff --git a/codemirror-overwrites/addon/search/match-highlighter.js b/vendor-overwrites/codemirror/addon/search/match-highlighter.js similarity index 100% rename from codemirror-overwrites/addon/search/match-highlighter.js rename to vendor-overwrites/codemirror/addon/search/match-highlighter.js diff --git a/codemirror/LICENSE b/vendor/codemirror/LICENSE similarity index 100% rename from codemirror/LICENSE rename to vendor/codemirror/LICENSE diff --git a/codemirror/addon/comment/comment.js b/vendor/codemirror/addon/comment/comment.js similarity index 100% rename from codemirror/addon/comment/comment.js rename to vendor/codemirror/addon/comment/comment.js diff --git a/codemirror/addon/dialog/dialog.css b/vendor/codemirror/addon/dialog/dialog.css similarity index 100% rename from codemirror/addon/dialog/dialog.css rename to vendor/codemirror/addon/dialog/dialog.css diff --git a/codemirror/addon/dialog/dialog.js b/vendor/codemirror/addon/dialog/dialog.js similarity index 100% rename from codemirror/addon/dialog/dialog.js rename to vendor/codemirror/addon/dialog/dialog.js diff --git a/codemirror/addon/edit/matchbrackets.js b/vendor/codemirror/addon/edit/matchbrackets.js similarity index 100% rename from codemirror/addon/edit/matchbrackets.js rename to vendor/codemirror/addon/edit/matchbrackets.js diff --git a/codemirror/addon/fold/brace-fold.js b/vendor/codemirror/addon/fold/brace-fold.js similarity index 100% rename from codemirror/addon/fold/brace-fold.js rename to vendor/codemirror/addon/fold/brace-fold.js diff --git a/codemirror/addon/fold/comment-fold.js b/vendor/codemirror/addon/fold/comment-fold.js similarity index 100% rename from codemirror/addon/fold/comment-fold.js rename to vendor/codemirror/addon/fold/comment-fold.js diff --git a/codemirror/addon/fold/foldcode.js b/vendor/codemirror/addon/fold/foldcode.js similarity index 100% rename from codemirror/addon/fold/foldcode.js rename to vendor/codemirror/addon/fold/foldcode.js diff --git a/codemirror/addon/fold/foldgutter.css b/vendor/codemirror/addon/fold/foldgutter.css similarity index 100% rename from codemirror/addon/fold/foldgutter.css rename to vendor/codemirror/addon/fold/foldgutter.css diff --git a/codemirror/addon/fold/foldgutter.js b/vendor/codemirror/addon/fold/foldgutter.js similarity index 100% rename from codemirror/addon/fold/foldgutter.js rename to vendor/codemirror/addon/fold/foldgutter.js diff --git a/codemirror/addon/hint/css-hint.js b/vendor/codemirror/addon/hint/css-hint.js similarity index 100% rename from codemirror/addon/hint/css-hint.js rename to vendor/codemirror/addon/hint/css-hint.js diff --git a/codemirror/addon/hint/show-hint.css b/vendor/codemirror/addon/hint/show-hint.css similarity index 100% rename from codemirror/addon/hint/show-hint.css rename to vendor/codemirror/addon/hint/show-hint.css diff --git a/codemirror/addon/hint/show-hint.js b/vendor/codemirror/addon/hint/show-hint.js similarity index 100% rename from codemirror/addon/hint/show-hint.js rename to vendor/codemirror/addon/hint/show-hint.js diff --git a/codemirror/addon/lint/css-lint.js b/vendor/codemirror/addon/lint/css-lint.js similarity index 100% rename from codemirror/addon/lint/css-lint.js rename to vendor/codemirror/addon/lint/css-lint.js diff --git a/codemirror/addon/lint/lint.css b/vendor/codemirror/addon/lint/lint.css similarity index 100% rename from codemirror/addon/lint/lint.css rename to vendor/codemirror/addon/lint/lint.css diff --git a/codemirror/addon/lint/lint.js b/vendor/codemirror/addon/lint/lint.js similarity index 100% rename from codemirror/addon/lint/lint.js rename to vendor/codemirror/addon/lint/lint.js diff --git a/codemirror/addon/scroll/annotatescrollbar.js b/vendor/codemirror/addon/scroll/annotatescrollbar.js similarity index 100% rename from codemirror/addon/scroll/annotatescrollbar.js rename to vendor/codemirror/addon/scroll/annotatescrollbar.js diff --git a/codemirror/addon/search/matchesonscrollbar.css b/vendor/codemirror/addon/search/matchesonscrollbar.css similarity index 100% rename from codemirror/addon/search/matchesonscrollbar.css rename to vendor/codemirror/addon/search/matchesonscrollbar.css diff --git a/codemirror/addon/search/matchesonscrollbar.js b/vendor/codemirror/addon/search/matchesonscrollbar.js similarity index 100% rename from codemirror/addon/search/matchesonscrollbar.js rename to vendor/codemirror/addon/search/matchesonscrollbar.js diff --git a/codemirror/addon/search/search.js b/vendor/codemirror/addon/search/search.js similarity index 100% rename from codemirror/addon/search/search.js rename to vendor/codemirror/addon/search/search.js diff --git a/codemirror/addon/search/searchcursor.js b/vendor/codemirror/addon/search/searchcursor.js similarity index 100% rename from codemirror/addon/search/searchcursor.js rename to vendor/codemirror/addon/search/searchcursor.js diff --git a/codemirror/addon/selection/active-line.js b/vendor/codemirror/addon/selection/active-line.js similarity index 100% rename from codemirror/addon/selection/active-line.js rename to vendor/codemirror/addon/selection/active-line.js diff --git a/codemirror/keymap/emacs.js b/vendor/codemirror/keymap/emacs.js similarity index 100% rename from codemirror/keymap/emacs.js rename to vendor/codemirror/keymap/emacs.js diff --git a/codemirror/keymap/sublime.js b/vendor/codemirror/keymap/sublime.js similarity index 100% rename from codemirror/keymap/sublime.js rename to vendor/codemirror/keymap/sublime.js diff --git a/codemirror/keymap/vim.js b/vendor/codemirror/keymap/vim.js similarity index 100% rename from codemirror/keymap/vim.js rename to vendor/codemirror/keymap/vim.js diff --git a/codemirror/lib/codemirror.css b/vendor/codemirror/lib/codemirror.css similarity index 100% rename from codemirror/lib/codemirror.css rename to vendor/codemirror/lib/codemirror.css diff --git a/codemirror/lib/codemirror.js b/vendor/codemirror/lib/codemirror.js similarity index 100% rename from codemirror/lib/codemirror.js rename to vendor/codemirror/lib/codemirror.js diff --git a/codemirror/mode/css/css.js b/vendor/codemirror/mode/css/css.js similarity index 100% rename from codemirror/mode/css/css.js rename to vendor/codemirror/mode/css/css.js diff --git a/codemirror/mode/css/gss.html b/vendor/codemirror/mode/css/gss.html similarity index 100% rename from codemirror/mode/css/gss.html rename to vendor/codemirror/mode/css/gss.html diff --git a/codemirror/mode/css/gss_test.js b/vendor/codemirror/mode/css/gss_test.js similarity index 100% rename from codemirror/mode/css/gss_test.js rename to vendor/codemirror/mode/css/gss_test.js diff --git a/codemirror/mode/css/index.html b/vendor/codemirror/mode/css/index.html similarity index 100% rename from codemirror/mode/css/index.html rename to vendor/codemirror/mode/css/index.html diff --git a/codemirror/mode/css/less.html b/vendor/codemirror/mode/css/less.html similarity index 100% rename from codemirror/mode/css/less.html rename to vendor/codemirror/mode/css/less.html diff --git a/codemirror/mode/css/less_test.js b/vendor/codemirror/mode/css/less_test.js similarity index 100% rename from codemirror/mode/css/less_test.js rename to vendor/codemirror/mode/css/less_test.js diff --git a/codemirror/mode/css/scss.html b/vendor/codemirror/mode/css/scss.html similarity index 100% rename from codemirror/mode/css/scss.html rename to vendor/codemirror/mode/css/scss.html diff --git a/codemirror/mode/css/scss_test.js b/vendor/codemirror/mode/css/scss_test.js similarity index 100% rename from codemirror/mode/css/scss_test.js rename to vendor/codemirror/mode/css/scss_test.js diff --git a/codemirror/mode/css/test.js b/vendor/codemirror/mode/css/test.js similarity index 100% rename from codemirror/mode/css/test.js rename to vendor/codemirror/mode/css/test.js diff --git a/codemirror/theme/3024-day.css b/vendor/codemirror/theme/3024-day.css similarity index 100% rename from codemirror/theme/3024-day.css rename to vendor/codemirror/theme/3024-day.css diff --git a/codemirror/theme/3024-night.css b/vendor/codemirror/theme/3024-night.css similarity index 100% rename from codemirror/theme/3024-night.css rename to vendor/codemirror/theme/3024-night.css diff --git a/codemirror/theme/abcdef.css b/vendor/codemirror/theme/abcdef.css similarity index 100% rename from codemirror/theme/abcdef.css rename to vendor/codemirror/theme/abcdef.css diff --git a/codemirror/theme/ambiance-mobile.css b/vendor/codemirror/theme/ambiance-mobile.css similarity index 100% rename from codemirror/theme/ambiance-mobile.css rename to vendor/codemirror/theme/ambiance-mobile.css diff --git a/codemirror/theme/ambiance.css b/vendor/codemirror/theme/ambiance.css similarity index 100% rename from codemirror/theme/ambiance.css rename to vendor/codemirror/theme/ambiance.css diff --git a/codemirror/theme/base16-dark.css b/vendor/codemirror/theme/base16-dark.css similarity index 100% rename from codemirror/theme/base16-dark.css rename to vendor/codemirror/theme/base16-dark.css diff --git a/codemirror/theme/base16-light.css b/vendor/codemirror/theme/base16-light.css similarity index 100% rename from codemirror/theme/base16-light.css rename to vendor/codemirror/theme/base16-light.css diff --git a/codemirror/theme/bespin.css b/vendor/codemirror/theme/bespin.css similarity index 100% rename from codemirror/theme/bespin.css rename to vendor/codemirror/theme/bespin.css diff --git a/codemirror/theme/blackboard.css b/vendor/codemirror/theme/blackboard.css similarity index 100% rename from codemirror/theme/blackboard.css rename to vendor/codemirror/theme/blackboard.css diff --git a/codemirror/theme/cobalt.css b/vendor/codemirror/theme/cobalt.css similarity index 100% rename from codemirror/theme/cobalt.css rename to vendor/codemirror/theme/cobalt.css diff --git a/codemirror/theme/colorforth.css b/vendor/codemirror/theme/colorforth.css similarity index 100% rename from codemirror/theme/colorforth.css rename to vendor/codemirror/theme/colorforth.css diff --git a/codemirror/theme/dracula.css b/vendor/codemirror/theme/dracula.css similarity index 100% rename from codemirror/theme/dracula.css rename to vendor/codemirror/theme/dracula.css diff --git a/codemirror/theme/duotone-dark.css b/vendor/codemirror/theme/duotone-dark.css similarity index 100% rename from codemirror/theme/duotone-dark.css rename to vendor/codemirror/theme/duotone-dark.css diff --git a/codemirror/theme/duotone-light.css b/vendor/codemirror/theme/duotone-light.css similarity index 100% rename from codemirror/theme/duotone-light.css rename to vendor/codemirror/theme/duotone-light.css diff --git a/codemirror/theme/eclipse.css b/vendor/codemirror/theme/eclipse.css similarity index 100% rename from codemirror/theme/eclipse.css rename to vendor/codemirror/theme/eclipse.css diff --git a/codemirror/theme/elegant.css b/vendor/codemirror/theme/elegant.css similarity index 100% rename from codemirror/theme/elegant.css rename to vendor/codemirror/theme/elegant.css diff --git a/codemirror/theme/erlang-dark.css b/vendor/codemirror/theme/erlang-dark.css similarity index 100% rename from codemirror/theme/erlang-dark.css rename to vendor/codemirror/theme/erlang-dark.css diff --git a/codemirror/theme/hopscotch.css b/vendor/codemirror/theme/hopscotch.css similarity index 100% rename from codemirror/theme/hopscotch.css rename to vendor/codemirror/theme/hopscotch.css diff --git a/codemirror/theme/icecoder.css b/vendor/codemirror/theme/icecoder.css similarity index 100% rename from codemirror/theme/icecoder.css rename to vendor/codemirror/theme/icecoder.css diff --git a/codemirror/theme/isotope.css b/vendor/codemirror/theme/isotope.css similarity index 100% rename from codemirror/theme/isotope.css rename to vendor/codemirror/theme/isotope.css diff --git a/codemirror/theme/lesser-dark.css b/vendor/codemirror/theme/lesser-dark.css similarity index 100% rename from codemirror/theme/lesser-dark.css rename to vendor/codemirror/theme/lesser-dark.css diff --git a/codemirror/theme/liquibyte.css b/vendor/codemirror/theme/liquibyte.css similarity index 100% rename from codemirror/theme/liquibyte.css rename to vendor/codemirror/theme/liquibyte.css diff --git a/codemirror/theme/material.css b/vendor/codemirror/theme/material.css similarity index 100% rename from codemirror/theme/material.css rename to vendor/codemirror/theme/material.css diff --git a/codemirror/theme/mbo.css b/vendor/codemirror/theme/mbo.css similarity index 100% rename from codemirror/theme/mbo.css rename to vendor/codemirror/theme/mbo.css diff --git a/codemirror/theme/mdn-like.css b/vendor/codemirror/theme/mdn-like.css similarity index 100% rename from codemirror/theme/mdn-like.css rename to vendor/codemirror/theme/mdn-like.css diff --git a/codemirror/theme/midnight.css b/vendor/codemirror/theme/midnight.css similarity index 100% rename from codemirror/theme/midnight.css rename to vendor/codemirror/theme/midnight.css diff --git a/codemirror/theme/monokai.css b/vendor/codemirror/theme/monokai.css similarity index 100% rename from codemirror/theme/monokai.css rename to vendor/codemirror/theme/monokai.css diff --git a/codemirror/theme/neat.css b/vendor/codemirror/theme/neat.css similarity index 100% rename from codemirror/theme/neat.css rename to vendor/codemirror/theme/neat.css diff --git a/codemirror/theme/neo.css b/vendor/codemirror/theme/neo.css similarity index 100% rename from codemirror/theme/neo.css rename to vendor/codemirror/theme/neo.css diff --git a/codemirror/theme/night.css b/vendor/codemirror/theme/night.css similarity index 100% rename from codemirror/theme/night.css rename to vendor/codemirror/theme/night.css diff --git a/codemirror/theme/panda-syntax.css b/vendor/codemirror/theme/panda-syntax.css similarity index 100% rename from codemirror/theme/panda-syntax.css rename to vendor/codemirror/theme/panda-syntax.css diff --git a/codemirror/theme/paraiso-dark.css b/vendor/codemirror/theme/paraiso-dark.css similarity index 100% rename from codemirror/theme/paraiso-dark.css rename to vendor/codemirror/theme/paraiso-dark.css diff --git a/codemirror/theme/paraiso-light.css b/vendor/codemirror/theme/paraiso-light.css similarity index 100% rename from codemirror/theme/paraiso-light.css rename to vendor/codemirror/theme/paraiso-light.css diff --git a/codemirror/theme/pastel-on-dark.css b/vendor/codemirror/theme/pastel-on-dark.css similarity index 100% rename from codemirror/theme/pastel-on-dark.css rename to vendor/codemirror/theme/pastel-on-dark.css diff --git a/codemirror/theme/railscasts.css b/vendor/codemirror/theme/railscasts.css similarity index 100% rename from codemirror/theme/railscasts.css rename to vendor/codemirror/theme/railscasts.css diff --git a/codemirror/theme/rubyblue.css b/vendor/codemirror/theme/rubyblue.css similarity index 100% rename from codemirror/theme/rubyblue.css rename to vendor/codemirror/theme/rubyblue.css diff --git a/codemirror/theme/seti.css b/vendor/codemirror/theme/seti.css similarity index 100% rename from codemirror/theme/seti.css rename to vendor/codemirror/theme/seti.css diff --git a/codemirror/theme/solarized.css b/vendor/codemirror/theme/solarized.css similarity index 100% rename from codemirror/theme/solarized.css rename to vendor/codemirror/theme/solarized.css diff --git a/codemirror/theme/the-matrix.css b/vendor/codemirror/theme/the-matrix.css similarity index 100% rename from codemirror/theme/the-matrix.css rename to vendor/codemirror/theme/the-matrix.css diff --git a/codemirror/theme/tomorrow-night-bright.css b/vendor/codemirror/theme/tomorrow-night-bright.css similarity index 100% rename from codemirror/theme/tomorrow-night-bright.css rename to vendor/codemirror/theme/tomorrow-night-bright.css diff --git a/codemirror/theme/tomorrow-night-eighties.css b/vendor/codemirror/theme/tomorrow-night-eighties.css similarity index 100% rename from codemirror/theme/tomorrow-night-eighties.css rename to vendor/codemirror/theme/tomorrow-night-eighties.css diff --git a/codemirror/theme/ttcn.css b/vendor/codemirror/theme/ttcn.css similarity index 100% rename from codemirror/theme/ttcn.css rename to vendor/codemirror/theme/ttcn.css diff --git a/codemirror/theme/twilight.css b/vendor/codemirror/theme/twilight.css similarity index 100% rename from codemirror/theme/twilight.css rename to vendor/codemirror/theme/twilight.css diff --git a/codemirror/theme/vibrant-ink.css b/vendor/codemirror/theme/vibrant-ink.css similarity index 100% rename from codemirror/theme/vibrant-ink.css rename to vendor/codemirror/theme/vibrant-ink.css diff --git a/codemirror/theme/xq-dark.css b/vendor/codemirror/theme/xq-dark.css similarity index 100% rename from codemirror/theme/xq-dark.css rename to vendor/codemirror/theme/xq-dark.css diff --git a/codemirror/theme/xq-light.css b/vendor/codemirror/theme/xq-light.css similarity index 100% rename from codemirror/theme/xq-light.css rename to vendor/codemirror/theme/xq-light.css diff --git a/codemirror/theme/yeti.css b/vendor/codemirror/theme/yeti.css similarity index 100% rename from codemirror/theme/yeti.css rename to vendor/codemirror/theme/yeti.css diff --git a/codemirror/theme/zenburn.css b/vendor/codemirror/theme/zenburn.css similarity index 100% rename from codemirror/theme/zenburn.css rename to vendor/codemirror/theme/zenburn.css diff --git a/csslint/WARNING.txt b/vendor/csslint/WARNING.txt similarity index 100% rename from csslint/WARNING.txt rename to vendor/csslint/WARNING.txt diff --git a/csslint/csslint-worker.js b/vendor/csslint/csslint-worker.js similarity index 100% rename from csslint/csslint-worker.js rename to vendor/csslint/csslint-worker.js