Răsfoiți Sursa

added iframe support

Gildas 7 ani în urmă
părinte
comite
b34ba0fe9e

+ 11 - 1
extension/core/scripts/bg/bg.js

@@ -18,7 +18,7 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global singlefile, chrome */
+/* global singlefile, chrome, FrameTree */
 
 (() => {
 
@@ -75,7 +75,17 @@
 	function processTab(tab, processOptions = {}) {
 		const options = singlefile.config.get();
 		Object.keys(processOptions).forEach(key => options[key] = processOptions[key]);
+		options.insertSingleFileComment = true;
 		singlefile.ui.init(tab.id);
+		if (options.removeFrames) {
+			processStart(tab, options);
+		} else {
+			FrameTree.initialize(tab.id)
+				.then(() => processStart(tab, options));
+		}
+	}
+
+	function processStart(tab, options) {
 		chrome.tabs.sendMessage(tab.id, { processStart: true, options });
 	}
 

+ 2 - 1
extension/core/scripts/bg/config.js

@@ -29,7 +29,8 @@ singlefile.config = (() => {
 		get() {
 			return localStorage.config ? JSON.parse(localStorage.config) : {
 				removeHidden: false,
-				removeUnusedCSSRules: false
+				removeUnusedCSSRules: false,
+				removeFrames: false
 			};
 		},
 		reset() {

+ 11 - 4
extension/core/scripts/content/client.js

@@ -18,7 +18,7 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global chrome, SingleFile, singlefile, document, Blob, MouseEvent, getSelection */
+/* global chrome, SingleFile, singlefile, FrameTree, document, Blob, MouseEvent, getSelection */
 
 (() => {
 
@@ -27,7 +27,12 @@
 	chrome.runtime.onMessage.addListener(request => {
 		if (request.processStart) {
 			fixInlineScripts();
-			SingleFile.process(getOptions(request.options))
+			getOptions(request.options)
+				.then(options => SingleFile.initialize(options))
+				.then(process => {
+					singlefile.ui.init();
+					return process();
+				})
 				.then(page => {
 					const date = new Date();
 					page.filename = page.title + " (" + date.toISOString().split("T")[0] + " " + date.toLocaleTimeString() + ")" + ".html";
@@ -39,7 +44,6 @@
 					chrome.runtime.sendMessage({ processError: true });
 					throw error;
 				});
-			singlefile.ui.init();
 		}
 	});
 
@@ -47,13 +51,16 @@
 		document.querySelectorAll("script").forEach(element => element.textContent = element.textContent.replace(/<\/script>/gi, "<\\/script>"));
 	}
 
-	function getOptions(options) {
+	async function getOptions(options) {
 		options.url = document.location.href;
 		if (options.selected) {
 			markSelectedContent();
 		}
 		options.content = getDoctype(document) + document.documentElement.outerHTML;
 		options.canvasData = getCanvasData();
+		if (!options.removeFrames) {
+			options.framesData = await FrameTree.getFramesData();
+		}
 		document.querySelectorAll("[" + SELECTED_CONTENT_ATTRIBUTE_NAME + "]").forEach(selectedContent => selectedContent.removeAttribute(SELECTED_CONTENT_ATTRIBUTE_NAME));
 		options.jsEnabled = true;
 		options.onprogress = onProgress;

+ 4 - 0
extension/ui/pages/options.html

@@ -11,6 +11,10 @@
 	<div>
 		<div id="popupContent">
 			<h4>Options</h4>
+			<div class="option">
+				<label for="removeFramesInput">remove frames</label>
+				<input type="checkbox" id="removeFramesInput">
+			</div>
 			<div class="option">
 				<label for="removeHiddenInput">remove hidden elements</label>
 				<input type="checkbox" id="removeHiddenInput">

+ 4 - 1
extension/ui/scripts/bg/options.js

@@ -25,6 +25,7 @@
 	const bgPage = chrome.extension.getBackgroundPage();
 	const removeHiddenInput = document.getElementById("removeHiddenInput");
 	const removeUnusedCSSRulesInput = document.getElementById("removeUnusedCSSRulesInput");
+	const removeFramesInput = document.getElementById("removeFramesInput");
 	document.getElementById("resetButton").addEventListener("click", () => {
 		bgPage.singlefile.config.reset();
 		refresh();
@@ -44,12 +45,14 @@
 		const config = bgPage.singlefile.config.get();
 		removeHiddenInput.checked = config.removeHidden;
 		removeUnusedCSSRulesInput.checked = config.removeUnusedCSSRules;
+		removeFramesInput.checked = config.removeFrames;
 	}
 
 	function update() {
 		bgPage.singlefile.config.set({
 			removeHidden: removeHiddenInput.checked,
-			removeUnusedCSSRules: removeUnusedCSSRulesInput.checked
+			removeUnusedCSSRules: removeUnusedCSSRulesInput.checked,
+			removeFrames: removeFramesInput.checked
 		});
 	}
 

+ 3 - 2
lib/single-file/single-file-browser.js

@@ -18,7 +18,7 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global SingleFileCore, base64, DOMParser, getComputedStyle, TextDecoder, window, fetch */
+/* global SingleFileCore, base64, DOMParser, getComputedStyle, TextDecoder, window, fetch, parseSrcset */
 
 this.SingleFile = (() => {
 
@@ -79,7 +79,8 @@ this.SingleFile = (() => {
 				DOMParser,
 				getComputedStyle,
 				document: doc,
-				serialize: () => getDoctype(doc) + doc.documentElement.outerHTML
+				serialize: () => getDoctype(doc) + doc.documentElement.outerHTML,
+				parseSrcSet: srcSet => parseSrcset(srcSet)
 			};
 		}
 	}

+ 70 - 11
lib/single-file/single-file-core.js

@@ -27,12 +27,14 @@ const SingleFileCore = (() => {
 	function SingleFileCore(...args) {
 		[Download, DOM, URL] = args;
 		return class {
-			static async process(options) {
+			static async initialize(options) {
 				const processor = new PageProcessor(options);
 				processor.onprogress = options.onprogress;
 				await processor.loadPage(options.content);
-				await processor.initialize();
-				return await processor.getContent();
+				return async () => {
+					await processor.initialize();
+					return await processor.getPageData();
+				};
 			}
 		};
 	}
@@ -80,11 +82,16 @@ const SingleFileCore = (() => {
 			if (!this.options.jsEnabled) {
 				this.processor.insertNoscriptContents();
 			}
+			if (this.options.removeFrames) {
+				this.processor.removeFrames();
+			}
 			this.processor.removeDiscardedResources();
 			this.processor.resetCharsetMeta();
 			this.processor.insertFaviconLink();
 			this.processor.resolveHrefs();
-			this.processor.insertSingleFileCommentNode();
+			if (this.options.insertSingleFileComment) {
+				this.processor.insertSingleFileCommentNode();
+			}
 			this.processor.replaceCanvasElements();
 			if (this.options.removeHiddenElements) {
 				this.processor.removeHiddenElements();
@@ -92,14 +99,18 @@ const SingleFileCore = (() => {
 			if (this.options.removeUnusedCSSRules) {
 				this.processor.removeUnusedCSSRules();
 			}
-			await Promise.all([this.processor.inlineStylesheets(true), this.processor.linkStylesheets()], this.processor.attributeStyles(true));
-			this.pendingPromises = Promise.all([this.processor.inlineStylesheets(), this.processor.attributeStyles(), this.processor.pageResources()]);
+			const initializationPromises = [this.processor.inlineStylesheets(true), this.processor.linkStylesheets(), this.processor.attributeStyles(true)];
+			if (!this.options.removeFrames) {
+				initializationPromises.push(this.processor.frames(true));
+			}
+			await Promise.all(initializationPromises);
+			this.pendingPromises = [this.processor.inlineStylesheets(), this.processor.attributeStyles(), this.processor.pageResources()];
 			if (this.onprogress) {
 				this.onprogress(new ProgressEvent(RESOURCES_INITIALIZED, { pageURL: this.options.url, index: 0, max: batchRequest.getMaxResources() }));
 			}
 		}
 
-		async getContent() {
+		async getPageData() {
 			await this.processor.retrieveResources(
 				details => {
 					if (this.onprogress) {
@@ -117,10 +128,13 @@ const SingleFileCore = (() => {
 			if (this.options.removeUnusedCSSRules) {
 				this.processor.removeUnusedCSSRules();
 			}
+			if (!this.options.removeFrames) {
+				await this.processor.frames();
+			}
 			if (this.onprogress) {
 				this.onprogress(new ProgressEvent(PAGE_ENDED, { pageURL: this.options.url }));
 			}
-			return this.processor.getContent();
+			return this.processor.getPageData();
 		}
 	}
 
@@ -209,7 +223,7 @@ const SingleFileCore = (() => {
 			await batchRequest.run(beforeListener, afterListener);
 		}
 
-		getContent() {
+		getPageData() {
 			if (this.options.selected) {
 				const selectedElement = this.doc.querySelector("[" + SELECTED_CONTENT_ATTRIBUTE_NAME + "]");
 				DomProcessorHelper.isolateElement(selectedElement.parentElement, selectedElement);
@@ -221,7 +235,7 @@ const SingleFileCore = (() => {
 				title = titleElement.textContent.trim();
 			}
 			return {
-				title: title || this.baseURI.match(/([^/]*)\/?$/),
+				title: title || (this.baseURI ? this.baseURI.match(/([^/]*)\/?$/) : ""),
 				content: this.dom.serialize()
 			};
 		}
@@ -243,11 +257,15 @@ const SingleFileCore = (() => {
 		}
 
 		removeDiscardedResources() {
-			this.doc.querySelectorAll("script, iframe, frame, applet, meta[http-equiv=refresh], object:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]), embed:not([src*=\".svg\"]), link[rel*=preload], link[rel*=prefetch]").forEach(element => element.remove());
+			this.doc.querySelectorAll("script, applet, meta[http-equiv=refresh], object:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]), embed:not([src*=\".svg\"]), link[rel*=preload], link[rel*=prefetch]").forEach(element => element.remove());
 			this.doc.querySelectorAll("[onload]").forEach(element => element.removeAttribute("onload"));
 			this.doc.querySelectorAll("audio[src], video[src]").forEach(element => element.removeAttribute("src"));
 		}
 
+		removeFrames() {
+			this.doc.querySelectorAll("iframe, frame").forEach(element => element.remove());
+		}
+
 		resetCharsetMeta() {
 			this.doc.querySelectorAll("meta[charset]").forEach(element => element.remove());
 			const metaElement = this.doc.createElement("meta");
@@ -364,6 +382,37 @@ const SingleFileCore = (() => {
 			}));
 		}
 
+		async frames(initialization) {
+			let frameElements = this.doc.querySelectorAll("iframe, frame");
+			frameElements = DomUtil.removeNoScriptFrames(frameElements);
+			await Promise.all(frameElements.map(async (frameElement, frameIndex) => {
+				const frameWindowId = (this.options.windowId || "0") + "." + frameIndex;
+				const frameData = this.options.framesData.find(frame => frame.windowId == frameWindowId);
+				if (frameData) {
+					if (initialization) {
+						const options = {
+							removeHiddenElements: this.options.removeHiddenElements,
+							removeUnusedCSSRules: this.options.removeUnusedCSSRules,
+							url: frameData.baseURI,
+							windowId: frameWindowId,
+							jsEnabled: this.options.jsEnabled,
+							insertSingleFileComment: false,
+							framesData: this.options.framesData
+						};
+						frameData.processor = new PageProcessor(options);
+						frameData.frameElement = frameElement;
+						await frameData.processor.loadPage(frameData.content);
+						return frameData.processor.initialize();
+					} else {
+						const pageData = await frameData.processor.getPageData();
+						frameElement.setAttribute("src", "data:text/html," + pageData.content);
+					}
+				} else {
+					frameElement.setAttribute("src", "about:blank");
+				}
+			}));
+		}
+
 		async attributeStyles(initialization) {
 			await Promise.all(Array.from(this.doc.querySelectorAll("[style]")).map(async element => {
 				const stylesheetContent = initialization ? await DomProcessorHelper.resolveImportURLs(element.getAttribute("style"), this.baseURI) : await DomProcessorHelper.processStylesheet(element.getAttribute("style"), this.baseURI);
@@ -571,6 +620,16 @@ const SingleFileCore = (() => {
 				return stylesheetContent;
 			}
 		}
+
+		static removeNoScriptFrames(frameElements) {
+			return Array.from(frameElements).filter(element => {
+				element = element.parentElement;
+				while (element && element.tagName != "NOSCRIPT") {
+					element = element.parentElement;
+				}
+				return !element;
+			});
+		}
 	}
 
 	return SingleFileCore;

+ 13 - 2
manifest.json

@@ -9,6 +9,7 @@
     "description": "Archive a complete page into a single HTML file",
     "background": {
         "scripts": [
+            "lib/frame-tree/bg/frame-tree.js",
             "extension/core/scripts/bg/index.js",
             "extension/core/scripts/bg/fetch.js",
             "extension/core/scripts/bg/config.js",
@@ -28,13 +29,13 @@
     "content_scripts": [
         {
             "matches": [
-                "http://*/*",
-                "https://*/*"
+                "<all_urls>"
             ],
             "js": [
                 "extension/ui/scripts/content/index.js",
                 "extension/ui/scripts/content/ui.js",
                 "lib/single-file/base64.js",
+                "lib/single-file/parse-srcset.js",
                 "lib/single-file/single-file-core.js",
                 "lib/single-file/single-file-browser.js",
                 "extension/core/scripts/content/fetch.js",
@@ -42,6 +43,16 @@
             ],
             "run_at": "document_start",
             "all_frames": false
+        },
+        {
+            "matches": [
+                "<all_urls>"
+            ],
+            "js": [
+                "lib/frame-tree/content/frame-tree.js"
+            ],
+            "run_at": "document_start",
+            "all_frames": true
         }
     ],
     "permissions": [