Browse Source

save changes made from Chrome devtools (cf. issue #244)

Former-commit-id: 12aa2d60531f413652f5933284170260b85c9722
Gildas 6 years ago
parent
commit
dbb863ba81

+ 1 - 0
extension/core/bg/business.js

@@ -105,6 +105,7 @@ singlefile.extension.core.bg.business = (() => {
 				ui.onStart(tabId, INJECT_SCRIPTS_STEP);
 				const tabOptions = await config.getOptions(tab.url);
 				Object.keys(options).forEach(key => tabOptions[key] = options[key]);
+				tabOptions.updatedResources = singlefile.extension.core.bg.devtools.getUpdatedResources(tabId);
 				let scriptsInjected;
 				if (!tabOptions.removeFrames) {
 					try {

+ 57 - 0
extension/core/bg/devtools.js

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2010-2019 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or 
+ *   modify it under the terms of the GNU Affero General Public License 
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ * 
+ *   The code in this file is distributed in the hope that it will be useful, 
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may 
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+ *   AGPL normally required by section 4, provided you include this license 
+ *   notice and a URL through which recipients can access the Corresponding 
+ *   Source.
+ */
+
+/* global singlefile */
+
+singlefile.extension.core.bg.devtools = (() => {
+
+	const updatedResources = {};
+
+	return {
+		onMessage,
+		onTabRemoved,
+		onTabUpdated,
+		getUpdatedResources: tabId => updatedResources[tabId]
+	};
+
+	async function onTabRemoved(tabId) {
+		delete updatedResources[tabId];
+	}
+
+	async function onTabUpdated(tabId) {
+		delete updatedResources[tabId];
+	}
+
+	function onMessage(message) {
+		if (message.method.endsWith(".resourceCommitted")) {
+			if (message.tabId && message.url && (message.type == "stylesheet" || message.type == "script")) {
+				const tabId = message.tabId;
+				if (!updatedResources[tabId]) {
+					updatedResources[tabId] = {};
+				}
+				updatedResources[tabId][message.url] = { content: message.content, type: message.type, encoding: message.encoding };
+			}
+		}
+	}
+
+})();

+ 3 - 0
extension/core/bg/messages.js

@@ -44,6 +44,9 @@ singlefile.extension.core.bg.messages = (() => {
 		if (message.method.startsWith("tabsData.")) {
 			return singlefile.extension.core.bg.tabsData.onMessage(message, sender);
 		}
+		if (message.method.startsWith("devtools.")) {
+			return singlefile.extension.core.bg.devtools.onMessage(message, sender);
+		}
 	});
 	if (browser.runtime.onMessageExternal) {
 		browser.runtime.onMessageExternal.addListener(async (message, sender) => {

+ 2 - 0
extension/core/bg/tabs.js

@@ -53,6 +53,7 @@ singlefile.extension.core.bg.tabs = (() => {
 	function onTabUpdated(tabId, changeInfo, tab) {
 		if (changeInfo.status == "loading") {
 			singlefile.extension.ui.bg.main.onTabUpdated(tabId, changeInfo, tab);
+			singlefile.extension.core.bg.devtools.onTabUpdated(tabId, changeInfo, tab);
 		}
 		if (changeInfo.status == "complete") {
 			singlefile.extension.core.bg.autosave.onTabUpdated(tabId, changeInfo, tab);
@@ -61,6 +62,7 @@ singlefile.extension.core.bg.tabs = (() => {
 
 	function onTabRemoved(tabId) {
 		singlefile.extension.core.bg.tabsData.onTabRemoved(tabId);
+		singlefile.extension.core.bg.devtools.onTabRemoved(tabId);
 	}
 
 })();

+ 13 - 0
extension/ui/devtools/devtools.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <title>DevTools page</title>
+</head>
+
+<body>
+    <script type="text/javascript" src="/lib/browser-polyfill/chrome-browser-polyfill.js"></script>
+    <script src="devtools.js"></script>
+</body>
+
+</html>

+ 16 - 0
extension/ui/devtools/devtools.js

@@ -0,0 +1,16 @@
+/* global browser */
+
+if (browser.devtools.inspectedWindow && browser.devtools.inspectedWindow.onResourceContentCommitted) {
+	browser.devtools.inspectedWindow.onResourceContentCommitted.addListener(resource => {
+		resource.getContent((content, encoding) => {
+			browser.runtime.sendMessage({
+				method: "devtools.resourceCommitted",
+				tabId: browser.devtools.inspectedWindow.tabId,
+				url: resource.url,
+				content,
+				encoding,
+				type: resource.type
+			});
+		});
+	});
+}

+ 10 - 0
lib/browser-polyfill/chrome-browser-polyfill.js

@@ -319,6 +319,16 @@
 						}
 					});
 				})
+			},
+			devtools: nativeAPI.devtools && {
+				inspectedWindow: nativeAPI.devtools.inspectedWindow && {
+					onResourceContentCommitted: nativeAPI.devtools.inspectedWindow.onResourceContentCommitted && {
+						addListener: listener => nativeAPI.devtools.inspectedWindow.onResourceContentCommitted.addListener(listener)
+					},
+					get tabId() {
+						return nativeAPI.devtools.inspectedWindow.tabId;
+					}
+				}
 			}
 		}));
 	}

+ 59 - 28
lib/single-file/single-file-core.js

@@ -155,6 +155,8 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 			this.options = options;
 			this.options.url = this.options.url || (this.options.doc && this.options.doc.location.href);
 			this.options.baseURI = this.options.doc && this.options.doc.baseURI;
+			this.options.rootDocument = root;
+			this.options.updatedResources = this.options.updatedResources || {};
 			this.batchRequest = new BatchRequest();
 			this.processor = new Processor(options, this.batchRequest);
 			if (this.options.doc) {
@@ -837,6 +839,15 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 		}
 
 		async resolveStylesheetURLs() {
+			const options = {
+				maxResourceSize: this.options.maxResourceSize,
+				maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
+				url: this.options.url,
+				charset: this.charset,
+				compressCSS: this.options.compressCSS,
+				updatedResources: this.options.updatedResources,
+				rootDocument: this.options.rootDocument
+			};
 			await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]"))
 				.map(async element => {
 					let mediaText;
@@ -844,43 +855,51 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 						mediaText = element.media.toLowerCase();
 					}
 					const stylesheetInfo = { mediaText };
-					this.stylesheets.set(element, stylesheetInfo);
 					if (element.closest("[" + SHADOW_MODE_ATTRIBUTE_NAME + "]")) {
 						stylesheetInfo.scoped = true;
 					}
-					const options = {
-						maxResourceSize: this.options.maxResourceSize,
-						maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
-						url: this.options.url,
-						charset: this.charset,
-						compressCSS: this.options.compressCSS
-					};
 					if (element.tagName.toLowerCase() == "link") {
 						if (element.charset) {
 							options.charset = element.charset;
 						}
 					}
-					let stylesheetContent = await getStylesheetContent(element, this.baseURI, options, this.workStyleElement);
-					const match = stylesheetContent.match(/^@charset\s+"([^"]*)";/i);
-					if (match && match[1] && match[1] != options.charset) {
-						options.charset = match[1];
-						stylesheetContent = await getStylesheetContent(element, this.baseURI, options, this.workStyleElement);
-					}
-					let stylesheet;
-					try {
-						stylesheet = cssTree.parse(Util.removeCssComments(stylesheetContent));
-					} catch (error) {
-						// ignored
-					}
-					if (stylesheet && stylesheet.children) {
-						if (this.options.compressCSS) {
-							ProcessorHelper.removeSingleLineCssComments(stylesheet);
-						}
-						stylesheetInfo.stylesheet = stylesheet;
-					} else {
-						this.stylesheets.delete(element);
-					}
+					await processElement(element, stylesheetInfo, this.stylesheets, this.baseURI, options, this.workStyleElement);
 				}));
+			if (options.rootDocument) {
+				const newResources = Object.keys(options.updatedResources).filter(url => options.updatedResources[url].type == "stylesheet" && !options.updatedResources[url].retrieved).map(url => options.updatedResources[url]);
+				await Promise.all(newResources.map(async resource => {
+					resource.retrieved = true;
+					const stylesheetInfo = {};
+					const element = this.doc.createElement("style");
+					this.doc.body.appendChild(element);
+					element.textContent = resource.content;
+					await processElement(element, stylesheetInfo, this.stylesheets, this.baseURI, options, this.workStyleElement);
+				}));
+			}
+
+			async function processElement(element, stylesheetInfo, stylesheets, baseURI, options, workStyleElement) {
+				stylesheets.set(element, stylesheetInfo);
+				let stylesheetContent = await getStylesheetContent(element, baseURI, options, workStyleElement);
+				const match = stylesheetContent.match(/^@charset\s+"([^"]*)";/i);
+				if (match && match[1] && match[1] != options.charset) {
+					options.charset = match[1];
+					stylesheetContent = await getStylesheetContent(element, baseURI, options, workStyleElement);
+				}
+				let stylesheet;
+				try {
+					stylesheet = cssTree.parse(Util.removeCssComments(stylesheetContent));
+				} catch (error) {
+					// ignored
+				}
+				if (stylesheet && stylesheet.children) {
+					if (options.compressCSS) {
+						ProcessorHelper.removeSingleLineCssComments(stylesheet);
+					}
+					stylesheetInfo.stylesheet = stylesheet;
+				} else {
+					stylesheets.delete(element);
+				}
+			}
 
 			async function getStylesheetContent(element, baseURI, options, workStyleElement) {
 				let content;
@@ -1113,6 +1132,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 						maxResourceSize: this.options.maxResourceSize,
 						maxResourceSizeEnabled: this.options.maxResourceSizeEnabled
 					});
+					content.data = Util.getUpdatedResourceContent(resourceURL, content, this.options);
 					scriptElement.setAttribute("src", content.data);
 				}
 			}));
@@ -1437,6 +1457,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 								validateTextContentType: true
 							});
 							resourceURL = content.resourceURL;
+							content.data = Util.getUpdatedResourceContent(resourceURL, content, options);
 							let importedStylesheetContent = Util.removeCssComments(content.data);
 							if (options.compressCSS) {
 								importedStylesheetContent = util.compressCSS(importedStylesheetContent);
@@ -1505,6 +1526,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 					charset: options.charset
 				});
 				resourceURL = content.resourceURL;
+				content.data = Util.getUpdatedResourceContent(content.resourceURL, content, options);
 				let stylesheetContent = Util.removeCssComments(content.data);
 				if (options.compressCSS) {
 					stylesheetContent = util.compressCSS(stylesheetContent);
@@ -1761,6 +1783,15 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 	const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
 
 	class Util {
+		static getUpdatedResourceContent(resourceURL, content, options) {
+			if (options.rootDocument && options.updatedResources[resourceURL]) {
+				options.updatedResources[resourceURL].retrieved = true;
+				return options.updatedResources[resourceURL].content;
+			} else {
+				return content.data || "";
+			}
+		}
+
 		static normalizeURL(url) {
 			if (!url || url.startsWith(DATA_URI_PREFIX)) {
 				return url;

+ 2 - 0
manifest.json

@@ -62,6 +62,7 @@
 			"extension/core/bg/downloads.js",
 			"extension/core/bg/autosave.js",
 			"extension/core/common/infobar.js",
+			"extension/core/bg/devtools.js",
 			"extension/ui/bg/ui-main.js",
 			"extension/ui/bg/ui-menus.js",
 			"extension/ui/bg/ui-commands.js",
@@ -141,6 +142,7 @@
 			"id": "{531906d3-e22f-4a6c-a102-8057b88a1a63}"
 		}
 	},
+	"devtools_page": "extension/ui/devtools/devtools.html",
 	"incognito": "spanning",
 	"manifest_version": 2,
 	"default_locale": "en"