Răsfoiți Sursa

added an option to display some stats in the console

Gildas 7 ani în urmă
părinte
comite
668841bd31

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

@@ -40,7 +40,8 @@ singlefile.config = (() => {
 		maxResourceSize: 10,
 		removeAudioSrc: true,
 		removeVideoSrc: true,
-		displayInfobar: true
+		displayInfobar: true,
+		displayStats: false
 	};
 
 	let pendingUpgradePromise;

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

@@ -93,6 +93,10 @@ this.singlefile.top = this.singlefile.top || (() => {
 		if (options.shadowEnabled) {
 			singlefile.ui.end();
 		}
+		if (options.displayStats) {
+			console.log("SingleFile stats"); // eslint-disable-line no-console
+			console.table(page.stats); // eslint-disable-line no-console
+		}
 		return page;
 	}
 

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

@@ -41,6 +41,7 @@
 	const removeAudioSrcInput = document.getElementById("removeAudioSrcInput");
 	const removeVideoSrcInput = document.getElementById("removeVideoSrcInput");
 	const displayInfobarInput = document.getElementById("displayInfobarInput");
+	const displayStatsInput = document.getElementById("displayStatsInput");
 	let pendingSave = Promise.resolve();
 	document.getElementById("resetButton").addEventListener("click", async () => {
 		await bgPage.singlefile.config.reset();
@@ -72,6 +73,7 @@
 		removeAudioSrcInput.checked = config.removeAudioSrc;
 		removeVideoSrcInput.checked = config.removeVideoSrc;
 		displayInfobarInput.checked = config.displayInfobar;
+		displayStatsInput.checked = config.displayStats;
 	}
 
 	async function update() {
@@ -94,7 +96,8 @@
 			confirmFilename: confirmFilenameInput.checked,
 			removeAudioSrc: removeAudioSrcInput.checked,
 			removeVideoSrc: removeVideoSrcInput.checked,
-			displayInfobar: displayInfobarInput.checked
+			displayInfobar: displayInfobarInput.checked,
+			displayStats: displayStatsInput.checked
 		});
 		await pendingSave;
 		await bgPage.singlefile.ui.update();

+ 9 - 0
extension/ui/pages/help.html

@@ -67,6 +67,15 @@
 							<u>check</u> this option</p>
 					</li>
 
+					<li>
+						<span class="option">display stats in the console after processing</span>
+						<p>Check this option to display stats about processing in the JavaScript developer tools of your browser. Checking this
+							option may increase the time needed to process a page.
+						</p>
+						<p class="notice">It is recommended to
+							<u>uncheck</u> this option</p>
+					</li>
+
 					<li>
 						<span class="option">append the save date to the file name</span>
 						<p>Check this option to append the save date of the webpage to the file name.

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

@@ -22,6 +22,10 @@
 			<label for="displayInfobarInput">display an infobar when viewing archives</label>
 			<input type="checkbox" id="displayInfobarInput">
 		</div>
+		<div class="option">
+			<label for="displayStatsInput">display stats in the console after processing</label>
+			<input type="checkbox" id="displayStatsInput">
+		</div>
 	</details>
 	<details>
 		<summary>File name</summary>

+ 8 - 0
lib/single-file/rules-minifier.js

@@ -25,14 +25,22 @@ this.rulesMinifier = this.rulesMinifier || (() => {
 	return {
 		process: doc => {
 			const rulesCache = {};
+			const stats = {
+				processed: 0,
+				discarded: 0
+			};
 			doc.querySelectorAll("style").forEach(style => {
 				const cssRules = [];
 				if (style.sheet) {
+					const processed = style.sheet.cssRules.length;
+					stats.processed += processed;
 					processRules(doc, style.sheet.cssRules, cssRules, rulesCache);
 					const stylesheetContent = cssRules.join("");
 					style.textContent = stylesheetContent;
+					stats.discarded += processed - style.sheet.cssRules.length;
 				}
 			});
+			return stats;
 		}
 	};
 

+ 129 - 10
lib/single-file/single-file-core.js

@@ -18,6 +18,8 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+/* global Blob */
+
 this.SingleFileCore = this.SingleFileCore || (() => {
 
 	const SELECTED_CONTENT_ATTRIBUTE_NAME = "data-single-file-selected-content";
@@ -214,6 +216,38 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 	class DOMProcessor {
 		constructor(options) {
 			this.options = options;
+			if (this.options.displayStats) {
+				this.stats = {
+					discarded: {
+						htmlBytes: 0,
+						hiddenElements: 0,
+						imports: 0,
+						scripts: 0,
+						objects: 0,
+						audioSource: 0,
+						videoSource: 0,
+						frames: 0,
+						cssRules: 0,
+						canvas: 0,
+						styleSheets: 0,
+						resources: 0
+					},
+					processed: {
+						htmlBytes: 0,
+						hiddenElements: 0,
+						imports: 0,
+						scripts: 0,
+						objects: 0,
+						audioSource: 0,
+						videoSource: 0,
+						frames: 0,
+						cssRules: 0,
+						canvas: 0,
+						styleSheets: 0,
+						resources: 0
+					}
+				};
+			}
 			this.baseURI = options.url;
 		}
 
@@ -240,6 +274,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 
 		async retrieveResources(onloadListener) {
+			if (this.options.displayStats) {
+				this.stats.processed.resources = batchRequest.getMaxResources();
+			}
 			await batchRequest.run(onloadListener, this.options);
 		}
 
@@ -258,9 +295,19 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 				title = titleElement.textContent.trim();
 			}
 			const matchTitle = this.baseURI.match(/([^/]*)\/?(\.html?.*)$/) || this.baseURI.match(/\/\/([^/]*)\/?$/);
+			let size;
+			if (this.options.displayStats) {
+				size = new Blob([this.doc.documentElement.outerHTML]).size;
+			}
+			const content = this.dom.serialize(this.options);
+			if (this.options.displayStats) {
+				this.stats.processed.htmlBytes = new Blob([content]).size;
+				this.stats.discarded.htmlBytes += size - this.stats.processed.htmlBytes;
+			}
 			return {
+				stats: this.stats,
 				title: title || (this.baseURI && matchTitle ? matchTitle[1] : ""),
-				content: this.dom.serialize(this.options)
+				content
 			};
 		}
 
@@ -285,14 +332,26 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 
 		removeDiscardedResources() {
-			this.doc.querySelectorAll("applet, meta[http-equiv=refresh], object:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]):not([type=\"text/html\"]), embed:not([src*=\".svg\"]), link[rel*=preload], link[rel*=prefetch]").forEach(element => element.remove());
+			const objectElements = this.doc.querySelectorAll("applet, meta[http-equiv=refresh], object:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]):not([type=\"text/html\"]), embed:not([src*=\".svg\"]), link[rel*=preload], link[rel*=prefetch]");
+			if (this.options.displayStats) {
+				this.stats.discarded.objects = objectElements.length;
+			}
+			objectElements.forEach(element => element.remove());
 			this.doc.querySelectorAll("[onload]").forEach(element => element.removeAttribute("onload"));
 			this.doc.querySelectorAll("[onerror]").forEach(element => element.removeAttribute("onerror"));
 			if (this.options.removeAudioSrc) {
-				this.doc.querySelectorAll("audio[src], audio > source[src]").forEach(element => element.removeAttribute("src"));
+				const audioSourceElements = this.doc.querySelectorAll("audio[src], audio > source[src]");
+				if (this.options.displayStats) {
+					this.stats.discarded.audioSource = objectElements.length;
+				}
+				audioSourceElements.forEach(element => element.removeAttribute("src"));
 			}
 			if (this.options.removeVideoSrc) {
-				this.doc.querySelectorAll("video[src], video > source[src]").forEach(element => element.removeAttribute("src"));
+				const videoSourceElements = this.doc.querySelectorAll("video[src], video > source[src]");
+				if (this.options.displayStats) {
+					this.stats.discarded.videoSource = objectElements.length;
+				}
+				videoSourceElements.forEach(element => element.removeAttribute("src"));
 			}
 		}
 
@@ -305,15 +364,27 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 
 		removeScripts() {
-			this.doc.querySelectorAll("script:not([type=\"application/ld+json\"])").forEach(element => element.remove());
+			const scriptElements = this.doc.querySelectorAll("script:not([type=\"application/ld+json\"])");
+			if (this.options.displayStats) {
+				this.stats.discarded.scripts = scriptElements.length;
+			}
+			scriptElements.forEach(element => element.remove());
 		}
 
 		removeFrames() {
+			const frameElements = this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]");
+			if (this.options.displayStats) {
+				this.stats.discarded.frames = frameElements.length;
+			}
 			this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]").forEach(element => element.remove());
 		}
 
 		removeImports() {
-			this.doc.querySelectorAll("link[rel=import]").forEach(element => element.remove());
+			const importElements = this.doc.querySelectorAll("link[rel=import]");
+			if (this.options.displayStats) {
+				this.stats.discarded.imports = importElements.length;
+			}
+			importElements.forEach(element => element.remove());
 		}
 
 		resetCharsetMeta() {
@@ -344,18 +415,40 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		}
 
 		removeUnusedCSSRules() {
-			this.dom.rulesMinifier(this.doc);
+			const stats = this.dom.rulesMinifier(this.doc);
+			if (this.options.displayStats) {
+				this.stats.processed.cssRules = stats.processed;
+				this.stats.discarded.cssRules = stats.discarded;
+			}
 		}
 
 		removeHiddenElements() {
-			this.doc.querySelectorAll("[" + REMOVED_CONTENT_ATTRIBUTE_NAME + "]").forEach(element => element.remove());
+			const hiddenElements = this.doc.querySelectorAll("[" + REMOVED_CONTENT_ATTRIBUTE_NAME + "]");
+			if (this.options.displayStats) {
+				this.stats.discarded.hiddenElements = hiddenElements.length;
+			}
+			hiddenElements.forEach(element => element.remove());
 		}
 
 		compressHTML(postProcess) {
 			if (postProcess) {
+				let size;
+				if (this.options.displayStats) {
+					size = new Blob([this.doc.documentElement.outerHTML]).size;
+				}
 				this.dom.htmlmini.postProcess(this.doc);
+				if (this.options.displayStats) {
+					this.stats.discarded.htmlBytes += size - (new Blob([this.doc.documentElement.outerHTML]).size);
+				}
 			} else {
+				let size;
+				if (this.options.displayStats) {
+					size = new Blob([this.doc.documentElement.outerHTML]).size;
+				}
 				this.dom.htmlmini.process(this.doc, { preservedSpaceAttributeName: PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME });
+				if (this.options.displayStats) {
+					this.stats.discarded.htmlBytes += size - (new Blob([this.doc.documentElement.outerHTML]).size);
+				}
 				this.doc.querySelectorAll("[" + PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME + "]").forEach(element => element.removeAttribute(PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME));
 			}
 		}
@@ -384,6 +477,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 							imgElement.style.pixelHeight = canvasData.height;
 						}
 						canvasElement.parentElement.replaceChild(imgElement, canvasElement);
+						if (this.options.displayStats) {
+							this.stats.processed.canvas++;
+						}
 					}
 				});
 			}
@@ -414,6 +510,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 
 		async inlineStylesheets(initialization) {
 			await Promise.all(Array.from(this.doc.querySelectorAll("style")).map(async styleElement => {
+				if (!initialization && this.options.displayStats) {
+					this.stats.processed.styleSheets++;
+				}
 				const stylesheetContent = initialization ? await DomProcessorHelper.resolveImportURLs(styleElement.textContent, this.baseURI, { maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled }) : await DomProcessorHelper.processStylesheet(styleElement.textContent, this.baseURI);
 				styleElement.textContent = !initialization && this.options.compressCSS ? this.dom.uglifycss(stylesheetContent) : stylesheetContent;
 			}));
@@ -422,6 +521,9 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 		async scripts() {
 			await Promise.all(Array.from(this.doc.querySelectorAll("script[src]")).map(async scriptElement => {
 				if (scriptElement.src) {
+					if (this.options.displayStats) {
+						this.stats.processed.scripts++;
+					}
 					const scriptContent = await Download.getContent(scriptElement.src, { asDataURI: false, maxResourceSize: this.options.maxResourceSize, maxResourceSizeEnabled: this.options.maxResourceSizeEnabled });
 					scriptElement.textContent = scriptContent.replace(/<\/script>/gi, "<\\/script>");
 				}
@@ -453,10 +555,19 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 							}
 						} else {
 							if (frameData.processor) {
+								if (this.options.displayStats) {
+									this.stats.processed.frames++;
+								}
 								await frameData.processor.preparePageData();
 								const pageData = await frameData.processor.getPageData();
 								frameElement.removeAttribute(WIN_ID_ATTRIBUTE_NAME);
 								DomProcessorHelper.setFrameContent(frameElement, pageData.content);
+								if (this.options.displayStats) {
+									Object.keys(this.stats.discarded).forEach(key => this.stats.discarded[key] += (pageData.stats.discarded[key] || 0));
+									Object.keys(this.stats.processed).forEach(key => this.stats.processed[key] += (pageData.stats.processed[key] || 0));
+								}
+							} else if (this.options.displayStats) {
+								this.stats.discarded.frames++;
 							}
 						}
 					}
@@ -485,13 +596,21 @@ this.SingleFileCore = this.SingleFileCore || (() => {
 						}
 					}
 				} else {
+					linkElement.setAttribute("href", EMPTY_DATA_URI);
 					const processor = this.relImportProcessors.get(linkElement);
 					if (processor) {
+						if (this.options.displayStats) {
+							this.stats.processed.imports++;
+						}
 						this.relImportProcessors.delete(linkElement);
 						const pageData = await processor.getPageData();
 						linkElement.setAttribute("href", "data:text/html," + pageData.content);
-					} else {
-						linkElement.setAttribute("href", EMPTY_DATA_URI);
+						if (this.options.displayStats) {
+							Object.keys(this.stats.discarded).forEach(key => this.stats.discarded[key] += (pageData.stats.discarded[key] || 0));
+							Object.keys(this.stats.processed).forEach(key => this.stats.processed[key] += (pageData.stats.processed[key] || 0));
+						}
+					} else if (this.options.displayStats) {
+						this.stats.discarded.imports++;
 					}
 				}
 			}));