From ac8331e6aef4c915c301938b4d642d633328b054 Mon Sep 17 00:00:00 2001 From: tophf Date: Mon, 13 Nov 2017 15:28:50 +0300 Subject: [PATCH] FF: use tabs API for XML viewer --- background/background.js | 5 + background/style-via-api.js | 228 ++++++++++++++++++++++++++++++++++++ content/apply.js | 15 +++ manifest.json | 1 + 4 files changed, 249 insertions(+) create mode 100644 background/style-via-api.js diff --git a/background/background.js b/background/background.js index 70302a99..60942b61 100644 --- a/background/background.js +++ b/background/background.js @@ -1,6 +1,7 @@ /* global dbExec, getStyles, saveStyle */ /* global handleCssTransitionBug */ /* global usercssHelper openEditor */ +/* global styleViaAPI */ 'use strict'; // eslint-disable-next-line no-var @@ -317,6 +318,10 @@ function onRuntimeMessage(request, sender, sendResponse) { .catch(() => sendResponse(false)); return KEEP_CHANNEL_OPEN; + case 'styleViaAPI': + styleViaAPI(request, sender); + return; + case 'download': download(request.url) .then(sendResponse) diff --git a/background/style-via-api.js b/background/style-via-api.js new file mode 100644 index 00000000..e7736a2b --- /dev/null +++ b/background/style-via-api.js @@ -0,0 +1,228 @@ +/* global getStyles */ +'use strict'; + +const styleViaAPI = !chrome.app && (() => { + const ACTIONS = { + styleApply, + styleDeleted, + styleUpdated, + styleAdded, + styleReplaceAll, + prefChanged, + }; + const NOP = Promise.resolve(new Error('NOP')); + const onError = () => {}; + + /* : Object + : Object + url: String, non-enumerable + : Array of strings + section code */ + const cache = new Map(); + + let observingTabs = false; + + return (request, sender) => { + const action = ACTIONS[request.action]; + return !action ? NOP : + action(request, sender) + .catch(onError) + .then(maybeToggleObserver); + }; + + function styleApply({id = null, ignoreUrlCheck}, {tab, frameId, url}) { + if (prefs.get('disableAll')) { + return NOP; + } + const {tabFrames, frameStyles} = getCachedData(tab.id, frameId); + if (id === null && !ignoreUrlCheck && frameStyles.url === url) { + return NOP; + } + return getStyles({id, matchUrl: url, enabled: true, asHash: true}).then(styles => { + const tasks = []; + for (const styleId in styles) { + if (isNaN(parseInt(styleId))) { + continue; + } + // shallow-extract code from the sections array in order to reuse references + // in other places whereas the combined string gets garbage-collected + const styleSections = styles[styleId].map(section => section.code); + const code = styleSections.join('\n'); + if (!code) { + delete frameStyles[styleId]; + continue; + } + if (code === (frameStyles[styleId] || []).join('\n')) { + continue; + } + frameStyles[styleId] = styleSections; + tasks.push( + browser.tabs.insertCSS(tab.id, { + code, + frameId, + runAt: 'document_start', + matchAboutBlank: true, + }).catch(onError)); + } + if (!removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles)) { + Object.defineProperty(frameStyles, 'url', {value: url, configurable: true}); + tabFrames[frameId] = frameStyles; + cache.set(tab.id, tabFrames); + } + return Promise.all(tasks); + }); + } + + function styleDeleted({id}, {tab, frameId}) { + const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id); + const code = styleSections.join('\n'); + if (code && !duplicateCodeExists({frameStyles, id, code})) { + delete frameStyles[id]; + removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles); + return removeCSS(tab.id, frameId, code); + } else { + return NOP; + } + } + + function styleUpdated({style}, sender) { + if (!style.enabled) { + return styleDeleted(style, sender); + } + const {tab, frameId} = sender; + const {frameStyles, styleSections} = getCachedData(tab.id, frameId, style.id); + const code = styleSections.join('\n'); + return styleApply(style, sender).then(code && (() => { + if (!duplicateCodeExists({frameStyles, code, id: null})) { + return removeCSS(tab.id, frameId, code); + } + })); + } + + function styleAdded({style}, sender) { + return style.enabled ? styleApply(style, sender) : NOP; + } + + function styleReplaceAll(request, sender) { + const {tab, frameId} = sender; + const oldStylesCode = getFrameStylesJoined(sender); + return styleApply({ignoreUrlCheck: true}, sender).then(() => { + const newStylesCode = getFrameStylesJoined(sender); + const tasks = oldStylesCode + .filter(code => !newStylesCode.includes(code)) + .map(code => removeCSS(tab.id, frameId, code)); + return Promise.all(tasks); + }); + } + + function prefChanged({prefs}, sender) { + if ('disableAll' in prefs) { + if (!prefs.disableAll) { + return styleApply({}, sender); + } + const {tab, frameId} = sender; + const {tabFrames, frameStyles} = getCachedData(tab.id, frameId); + if (isEmpty(frameStyles)) { + return NOP; + } + removeFrameIfEmpty(tab.id, frameId, tabFrames, {}); + const tasks = Object.keys(frameStyles) + .map(id => removeCSS(tab.id, frameId, frameStyles[id].join('\n'))); + return Promise.all(tasks); + } + } + + /* utilities */ + + function maybeToggleObserver() { + let method; + if (!observingTabs && cache.size) { + method = 'addListener'; + } else if (observingTabs && !cache.size) { + method = 'removeListener'; + } else { + return; + } + observingTabs = !observingTabs; + chrome.webNavigation.onCommitted[method](onNavigationCommitted); + chrome.tabs.onRemoved[method](onTabRemoved); + chrome.tabs.onReplaced[method](onTabReplaced); + } + + function onNavigationCommitted({tabId, frameId}) { + if (frameId === 0) { + onTabRemoved(tabId); + return; + } + const tabFrames = cache.get(tabId); + if (frameId in tabFrames) { + delete tabFrames[frameId]; + if (isEmpty(tabFrames)) { + onTabRemoved(tabId); + } + } + } + + function onTabRemoved(tabId) { + cache.delete(tabId); + maybeToggleObserver(); + } + + function onTabReplaced(addedTabId, removedTabId) { + onTabRemoved(removedTabId); + } + + function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) { + if (isEmpty(frameStyles)) { + delete tabFrames[frameId]; + if (isEmpty(tabFrames)) { + cache.delete(tabId); + } + return true; + } + } + + function getCachedData(tabId, frameId, styleId) { + const tabFrames = cache.get(tabId) || {}; + const frameStyles = tabFrames[frameId] || {}; + const styleSections = styleId && frameStyles[styleId] || []; + return {tabFrames, frameStyles, styleSections}; + } + + function getFrameStylesJoined({ + tab, + frameId, + frameStyles = getCachedData(tab.id, frameId).frameStyles, + }) { + return Object.keys(frameStyles).map(id => frameStyles[id].join('\n')); + } + + function duplicateCodeExists({ + tab, + frameId, + frameStyles = getCachedData(tab.id, frameId).frameStyles, + frameStylesCode = {}, + id, + code = frameStylesCode[id] || frameStyles[id].join('\n'), + }) { + id = String(id); + for (const styleId in frameStyles) { + if (id !== styleId && + code === (frameStylesCode[styleId] || frameStyles[styleId].join('\n'))) { + return true; + } + } + } + + function removeCSS(tabId, frameId, code) { + return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true}) + .catch(onError); + } + + function isEmpty(obj) { + for (const k in obj) { + return false; + } + return true; + } +})(); diff --git a/content/apply.js b/content/apply.js index ef47be7b..0d61e475 100644 --- a/content/apply.js +++ b/content/apply.js @@ -21,6 +21,10 @@ if (!isOwnPage) { } function requestStyles(options, callback = applyStyles) { + if (!chrome.app && document instanceof XMLDocument) { + chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'}); + return; + } var matchUrl = location.href; if (!matchUrl.match(/^(http|file|chrome|ftp)/)) { // dynamic about: and javascript: iframes don't have an URL yet @@ -57,6 +61,17 @@ function applyOnMessage(request, sender, sendResponse) { return; } + if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') { + request.action = request.method; + request.method = 'styleViaAPI'; + request.styles = null; + if (request.style) { + request.style.sections = null; + } + chrome.runtime.sendMessage(request); + return; + } + switch (request.method) { case 'styleDeleted': removeStyle(request); diff --git a/manifest.json b/manifest.json index 5d2c9eff..707fe45f 100644 --- a/manifest.json +++ b/manifest.json @@ -30,6 +30,7 @@ "js/script-loader.js", "background/background.js", "vendor/node-semver/semver.js", + "background/style-via-api.js", "background/update.js" ] },