Explorar o código

lazy-load content scripts

Gildas %!s(int64=7) %!d(string=hai) anos
pai
achega
6b6ea36b20

+ 32 - 1
extension/core/bg/bg.js

@@ -24,6 +24,27 @@ singlefile.core = (() => {
 
 	const TIMEOUT_PROCESS_START_MESSAGE = 3000;
 
+	const initScriptFiles = [
+		"/lib/browser-polyfill/custom-browser-polyfill.js",
+		"/extension/index.js",
+		"/lib/frame-tree/content/frame-tree.js",
+		"/extension/core/content/content-frame.js"
+	];
+
+	const processScriptFiles = [
+		"/lib/browser-polyfill/custom-browser-polyfill.js",
+		"/extension/index.js",
+		"/extension/ui/content/ui.js",
+		"/lib/single-file/base64.js",
+		"/lib/single-file/uglifycss.js",
+		"/lib/single-file/htmlnano.js",
+		"/lib/single-file/parse-srcset.js",
+		"/lib/single-file/single-file-core.js",
+		"/lib/single-file/single-file-browser.js",
+		"/lib/fetch/content/fetch.js",
+		"/extension/core/content/content.js"
+	];
+
 	return {
 		async processTab(tab, processOptions = {}) {
 			const options = await singlefile.config.get();
@@ -31,9 +52,10 @@ singlefile.core = (() => {
 			options.insertSingleFileComment = true;
 			options.insertFaviconLink = true;
 			return new Promise(async (resolve, reject) => {
+				const processPromise = processStart(tab, options);
 				const errorTimeout = setTimeout(reject, TIMEOUT_PROCESS_START_MESSAGE);
 				try {
-					await processStart(tab, options);
+					await processPromise;
 				} catch (error) {
 					reject(error);
 				}
@@ -45,8 +67,10 @@ singlefile.core = (() => {
 
 	async function processStart(tab, options) {
 		if (!options.removeFrames) {
+			await executeScripts(tab.id, initScriptFiles, { allFrames: true, matchAboutBlank: true });
 			await FrameTree.initialize(tab.id);
 		}
+		await executeScripts(tab.id, processScriptFiles, { allFrames: false });
 		if (options.frameId) {
 			await browser.tabs.sendMessage(tab.id, { processStart: true, options }, { frameId: options.frameId });
 		} else {
@@ -54,4 +78,11 @@ singlefile.core = (() => {
 		}
 	}
 
+	async function executeScripts(tabId, scriptFiles, details) {
+		for (let file of scriptFiles) {
+			details.file = file;
+			await browser.tabs.executeScript(tabId, details);
+		}
+	}
+
 })();

+ 2 - 1
extension/core/content/content-frame.js

@@ -20,7 +20,7 @@
 
 /* global browser, window, top, document */
 
-(() => {
+this.singlefile.frame = this.singlefile.frame || (() => {
 
 	if (window != top) {
 		browser.runtime.onMessage.addListener(async message => {
@@ -33,6 +33,7 @@
 			}
 		});
 	}
+	return true;
 
 	function getDoctype(doc) {
 		const docType = doc.doctype;

+ 3 - 4
extension/core/content/content.js

@@ -20,23 +20,22 @@
 
 /* global browser, SingleFile, singlefile, FrameTree, document, Blob, MouseEvent, getSelection, getComputedStyle, prompt, addEventListener */
 
-(() => {
+this.singlefile.top = this.singlefile.top || (() => {
 
 	const PROGRESS_LOADED_COEFFICIENT = 2;
 
 	let processing = false;
-
 	browser.runtime.onMessage.addListener(async message => {
 		savePage(message);
 		return {};
 	});
-
 	addEventListener("message", event => {
 		if (typeof event.data === "string" && event.data.startsWith("__SingleFile__::")) {
 			const message = JSON.parse(event.data.substring("__SingleFile__".length + 2));
 			savePage(message);
 		}
 	});
+	return true;
 
 	async function savePage(message) {
 		if (message.processStart && !processing && !message.options.frameId) {
@@ -55,7 +54,7 @@
 
 	async function processMessage(message) {
 		const options = await getOptions(message.options);
-		const processor = new SingleFile(options);
+		const processor = new (SingleFile.getClass())(options);
 		fixInlineScripts();
 		fixHeadNoScripts();
 		if (options.selected) {

+ 1 - 1
extension/index.js

@@ -20,4 +20,4 @@
 
 /* global */
 
-this.singlefile = {};
+this.singlefile = this.singlefile || {};

+ 1 - 1
extension/ui/content/ui.js

@@ -20,7 +20,7 @@
 
 /* global document */
 
-this.singlefile.ui = (() => {
+this.singlefile.ui = this.singlefile.ui || (() => {
 
 	const MASK_TAGNAME = "singlefile-mask";
 

+ 10 - 1
lib/browser-polyfill/custom-browser-polyfill.js

@@ -24,7 +24,7 @@
 
 	const isChrome = navigator.userAgent.includes("Chrome");
 
-	if (isChrome) {
+	if (isChrome && !this.browser) {
 		this.browser = {
 			browserAction: {
 				onClicked: {
@@ -156,6 +156,15 @@
 				onRemoved: {
 					addListener: listener => chrome.tabs.onRemoved.addListener(listener)
 				},
+				executeScript: (tabId, details) => new Promise((resolve, reject) => {
+					chrome.tabs.executeScript(tabId, details, () => {
+						if (chrome.runtime.lastError) {
+							reject(chrome.runtime.lastError);
+						} else {
+							resolve();
+						}
+					});
+				}),
 				sendMessage: (tabId, message, options = {}) => new Promise((resolve, reject) =>
 					chrome.tabs.sendMessage(tabId, message, options, response => {
 						if (chrome.runtime.lastError) {

+ 12 - 10
lib/fetch/content/fetch.js

@@ -20,17 +20,19 @@
 
 /* global browser */
 
-this.superFetch = (() => {
+this.superFetch = this.superFetch || (() => {
 
-	return async url => {
-		const responseFetch = await sendMessage({ method: "fetch", url });
-		return {
-			headers: { get: headerName => responseFetch.headers[headerName] },
-			arrayBuffer: async () => {
-				const response = await sendMessage({ method: "fetch.array", requestId: responseFetch.responseId });
-				return new Uint8Array(response.array).buffer;
-			}
-		};
+	return {
+		fetch: async url => {
+			const responseFetch = await sendMessage({ method: "fetch", url });
+			return {
+				headers: { get: headerName => responseFetch.headers[headerName] },
+				arrayBuffer: async () => {
+					const response = await sendMessage({ method: "fetch.array", requestId: responseFetch.responseId });
+					return new Uint8Array(response.array).buffer;
+				}
+			};
+		}
 	};
 
 	async function sendMessage(message) {

+ 1 - 1
lib/frame-tree/content/frame-tree.js

@@ -20,7 +20,7 @@
 
 /* global browser, window, top, document, HTMLHtmlElement, addEventListener */
 
-this.FrameTree = (() => {
+this.FrameTree = this.FrameTree || (() => {
 
 	const MESSAGE_PREFIX = "__FrameTree__";
 	const TIMEOUT_INIT_REQUEST_MESSAGE = 1000;

+ 1 - 1
lib/single-file/base64.js

@@ -24,7 +24,7 @@
 
 // derived from the work of Jameson Little - https://github.com/beatgammit/base64-js
 
-this.base64 = (() => {
+this.base64 = this.base64 || (() => {
 
 	const lookup = [];
 	const revLookup = [];

+ 69 - 67
lib/single-file/htmlnano.js

@@ -29,7 +29,7 @@
 
 /* global Node, NodeFilter */
 
-this.htmlnano = (() => {
+this.htmlnano = this.htmlnano || (() => {
 
 	// Source: https://github.com/kangax/html-minifier/issues/63
 	const booleanAttributes = [
@@ -76,6 +76,74 @@ this.htmlnano = (() => {
 		"visible"
 	];
 
+	const noWhitespaceCollapseElements = ["script", "style", "pre", "textarea"];
+
+	// Source: https://www.w3.org/TR/html4/sgml/dtd.html#events (Generic Attributes)
+	const safeToRemoveAttrs = [
+		"id",
+		"class",
+		"style",
+		"title",
+		"lang",
+		"dir",
+		"onclick",
+		"ondblclick",
+		"onmousedown",
+		"onmouseup",
+		"onmouseover",
+		"onmousemove",
+		"onmouseout",
+		"onkeypress",
+		"onkeydown",
+		"onkeyup"
+	];
+
+	const redundantAttributes = {
+		"form": {
+			"method": "get"
+		},
+		"input": {
+			"type": "text"
+		},
+		"button": {
+			"type": "submit"
+		},
+		"script": {
+			"language": "javascript",
+			"type": "text/javascript",
+			// Remove attribute if the function returns false
+			"charset": node => {
+				// The charset attribute only really makes sense on “external” SCRIPT elements:
+				// http://perfectionkills.com/optimizing-html/#8_script_charset
+				return !node.getAttribute("src");
+			}
+		},
+		"style": {
+			"media": "all",
+			"type": "text/css"
+		},
+		"link": {
+			"media": "all"
+		}
+	};
+
+	const modules = [collapseBooleanAttributes, collapseWhitespace, removeComments, removeEmptyAttributes, removeRedundantAttributes];
+
+	return {
+		process: doc => {
+			const nodesWalker = doc.createTreeWalker(doc.documentElement, NodeFilter.SHOW_ALL, null, false);
+			let node = nodesWalker.nextNode();
+			while (node) {
+				const deletedNode = modules.find(module => module(node));
+				const previousNode = node;
+				node = nodesWalker.nextNode();
+				if (deletedNode) {
+					previousNode.remove();
+				}
+			}
+		}
+	};
+
 	function collapseBooleanAttributes(node) {
 		if (node.nodeType == Node.ELEMENT_NODE) {
 			node.getAttributeNames().forEach(attributeName => {
@@ -86,8 +154,6 @@ this.htmlnano = (() => {
 		}
 	}
 
-	const noWhitespaceCollapseElements = ["script", "style", "pre", "textarea"];
-
 	/** Collapses redundant whitespaces */
 	function collapseWhitespace(node) {
 		if (node.nodeType == Node.TEXT_NODE) {
@@ -122,26 +188,6 @@ this.htmlnano = (() => {
 		}
 	}
 
-	// Source: https://www.w3.org/TR/html4/sgml/dtd.html#events (Generic Attributes)
-	const safeToRemoveAttrs = [
-		"id",
-		"class",
-		"style",
-		"title",
-		"lang",
-		"dir",
-		"onclick",
-		"ondblclick",
-		"onmousedown",
-		"onmouseup",
-		"onmouseover",
-		"onmousemove",
-		"onmouseout",
-		"onkeypress",
-		"onkeydown",
-		"onkeyup"
-	];
-
 	/** Removes empty attributes */
 	function removeEmptyAttributes(node) {
 		if (node.nodeType == Node.ELEMENT_NODE) {
@@ -156,35 +202,6 @@ this.htmlnano = (() => {
 		}
 	}
 
-	const redundantAttributes = {
-		"form": {
-			"method": "get"
-		},
-		"input": {
-			"type": "text"
-		},
-		"button": {
-			"type": "submit"
-		},
-		"script": {
-			"language": "javascript",
-			"type": "text/javascript",
-			// Remove attribute if the function returns false
-			"charset": node => {
-				// The charset attribute only really makes sense on “external” SCRIPT elements:
-				// http://perfectionkills.com/optimizing-html/#8_script_charset
-				return node.attrs && !node.attrs.src;
-			}
-		},
-		"style": {
-			"media": "all",
-			"type": "text/css"
-		},
-		"link": {
-			"media": "all"
-		}
-	};
-
 	/** Removes redundant attributes */
 	function removeRedundantAttributes(node) {
 		if (node.nodeType == Node.ELEMENT_NODE) {
@@ -200,19 +217,4 @@ this.htmlnano = (() => {
 		}
 	}
 
-	const modules = [collapseBooleanAttributes, collapseWhitespace, removeComments, removeEmptyAttributes, removeRedundantAttributes];
-
-	return doc => {
-		const nodesWalker = doc.createTreeWalker(doc.documentElement, NodeFilter.SHOW_ALL, null, false);
-		let node = nodesWalker.nextNode();
-		while (node) {
-			const deletedNode = modules.find(module => module(node));
-			const previousNode = node;
-			node = nodesWalker.nextNode();
-			if (deletedNode) {
-				previousNode.remove();
-			}
-		}
-	};
-
 })();

+ 7 - 3
lib/single-file/parse-srcset.js

@@ -14,10 +14,14 @@
  * (except for comments in parens).
  */
 
-this.parseSrcset = (() => {
+this.parseSrcset = this.parseSrcset || (() => {
+
+	return {
+		process
+	};
 
 	// 1. Let input be the value passed to this algorithm.
-	return input => {
+	function process(input) {
 
 		// UTILITY FUNCTIONS
 
@@ -308,6 +312,6 @@ this.parseSrcset = (() => {
 			}
 		} // (close parseDescriptors fn)
 
-	};
+	}
 
 })();

+ 5 - 5
lib/single-file/single-file-browser.js

@@ -20,7 +20,7 @@
 
 /* global SingleFileCore, base64, DOMParser, TextDecoder, fetch, superFetch, parseSrcset, uglifycss, htmlnano */
 
-this.SingleFile = (() => {
+this.SingleFile = this.SingleFile || (() => {
 
 	const ONE_MB = 1024 * 1024;
 
@@ -33,7 +33,7 @@ this.SingleFile = (() => {
 		static async getContent(resourceURL, options) {
 			let resourceContent;
 			if (!fetchResource) {
-				fetchResource = typeof superFetch == "undefined" ? fetch : superFetch;
+				fetchResource = typeof superFetch == "undefined" ? fetch : superFetch.fetch;
 			}
 			try {
 				resourceContent = await fetchResource(resourceURL);
@@ -93,9 +93,9 @@ this.SingleFile = (() => {
 				DOMParser,
 				document: doc,
 				serialize: () => getDoctype(doc) + doc.documentElement.outerHTML,
-				parseSrcset: srcset => parseSrcset(srcset),
+				parseSrcset: srcset => parseSrcset.process(srcset),
 				uglifycss: (content, options) => uglifycss.processString(content, options),
-				htmlnano: doc => htmlnano(doc)
+				htmlnano: doc => htmlnano.process(doc)
 			};
 		}
 	}
@@ -118,6 +118,6 @@ this.SingleFile = (() => {
 		return "";
 	}
 
-	return SingleFileCore(Download, DOM, URL);
+	return { getClass: () => SingleFileCore.getClass(Download, DOM, URL) };
 
 })();

+ 3 - 3
lib/single-file/single-file-core.js

@@ -18,14 +18,14 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-this.SingleFileCore = (() => {
+this.SingleFileCore = this.SingleFileCore || (() => {
 
 	const SELECTED_CONTENT_ATTRIBUTE_NAME = "data-single-file-selected-content";
 	const REMOVED_CONTENT_ATTRIBUTE_NAME = "data-single-file-removed-content";
 
 	let Download, DOM, URL;
 
-	function SingleFileCore(...args) {
+	function getClass(...args) {
 		[Download, DOM, URL] = args;
 		return class {
 			constructor(options) {
@@ -804,6 +804,6 @@ this.SingleFileCore = (() => {
 		}
 	}
 
-	return SingleFileCore;
+	return { getClass };
 
 })();

+ 1 - 1
lib/single-file/uglifycss.js

@@ -31,7 +31,7 @@
  * by Yahoo! Inc. under the BSD (revised) open source license.
  */
 
-this.uglifycss = (() => {
+this.uglifycss = this.uglifycss || (() => {
 
 	/**
 	 * @type {string} - placeholder prefix

+ 1 - 36
manifest.json

@@ -29,47 +29,12 @@
         "default_icon": "extension/ui/resources/icon_16.png",
         "default_title": "Save page with SingleFile"
     },
-    "content_scripts": [
-        {
-            "matches": [
-                "<all_urls>"
-            ],
-            "js": [
-                "lib/browser-polyfill/custom-browser-polyfill.js",
-                "extension/index.js",
-                "extension/ui/content/ui.js",
-                "lib/single-file/base64.js",
-                "lib/single-file/htmlnano.js",
-                "lib/single-file/uglifycss.js",
-                "lib/single-file/parse-srcset.js",
-                "lib/single-file/single-file-core.js",
-                "lib/single-file/single-file-browser.js",
-                "lib/fetch/content/fetch.js",
-                "extension/core/content/content.js"
-            ],
-            "run_at": "document_start",
-            "all_frames": false
-        },
-        {
-            "matches": [
-                "<all_urls>"
-            ],
-            "js": [
-                "lib/browser-polyfill/custom-browser-polyfill.js",
-                "lib/frame-tree/content/frame-tree.js",
-                "extension/core/content/content-frame.js"
-            ],
-            "run_at": "document_start",
-            "all_frames": true
-        }
-    ],
     "permissions": [
         "tabs",
         "contextMenus",
         "menus",
         "storage",
-        "http://*/*",
-        "https://*/*"
+        "<all_urls>"
     ],
     "applications": {
         "gecko": {