Ver Fonte

Merge pull request #7 from gildas-lormeau/master

upd
solokot há 5 anos atrás
pai
commit
7073f1f458

+ 2 - 1
README.MD

@@ -85,8 +85,9 @@ See https://addons.mozilla.org/firefox/addon/single-file/versions/
 - Firefox:
   - The "File name > file name conflict resolution" option does not work if set to "prompt for a name"
   - Sometimes, SingleFile is unable to save the contents of  sandboxed iframes because of [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1411641).
+  - When processing a page from the filesystem, external resources (e.g. images, stylesheets, fonts etc.) will not be embedded into the saved page. You can find more info about this bug [here](https://bugzilla.mozilla.org/show_bug.cgi?id=1644488). This bug has been closed by Mozilla as "WontFix".
 - Waterfox
-  - When opening pages saved with the option "Images > group duplicate images together" enabled, some duplicate images might not displayed. It is recommended to disable this option.
+  - When opening pages saved with the option "Images > group duplicate images together" enabled, some duplicate images might not displayed. It is recommended to disable this option.  
 
 ## Troubleshooting unknown issues
 Please follow these steps if you find an unknown issue:

+ 4 - 0
_locales/de/messages.json

@@ -235,6 +235,10 @@
 		"message": "maximale Inaktivitätszeit (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "Seite verkleinern",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "Bilder für alternative Bildschirmauflösungen entfernen",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/en/messages.json

@@ -235,6 +235,10 @@
 		"message": "maximum idle time (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "remove images for alternative screen resolutions",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/es/messages.json

@@ -235,6 +235,10 @@
 		"message": "periodo máximo de espera (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "alejar la página",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "eliminar imágenes para resoluciones alternativas de pantalla",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/fr/messages.json

@@ -235,6 +235,10 @@
 		"message": "temps d'inactivité maximal (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "dézoomer la page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "supprimer les images pour des résolutions d'écran alternatives",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 12 - 8
_locales/ja/messages.json

@@ -172,7 +172,7 @@
 		"description": "Options page label: 'template'"
 	},
 	"optionFilenameMaxLength": {
-		"message": "max length (bytes)",
+		"message": "最大長(バイト)",
 		"description": "Options page label: 'max length (bytes)'"
 	},
 	"optionConfirmFilename": {
@@ -196,10 +196,10 @@
 		"description": "Value for 'name conflict resolution' option: 'prompt for a name'"
 	},
 	"optionFilenameConflictActionSkip": {
-        "message": "skip duplicate files",
-        "description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
-    },
-    "optionsHTMLContentSubTitle": {
+		"message": "重複ファイルをスキップ",
+		"description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
+	},
+	"optionsHTMLContentSubTitle": {
 		"message": "HTML コンテンツ",
 		"description": "Options sub-title: 'HTML content'"
 	},
@@ -235,6 +235,10 @@
 		"message": "最大アイドル時間(ミリ秒)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "代替画面解像度用の画像を削除します",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"
@@ -376,7 +380,7 @@
 		"description": "Options page label: 'save pages in background'"
 	},
 	"optionApplySystemTheme": {
-		"message": "apply the system theme when formatting a page in the annotation editor",
+		"message": "(注釈エディタ)でページをフォーマットするときにシステムテーマを適用する",
 		"description": "Title of the button 'apply the system theme when formatting a page in the annotation editor"
 	},
 	"optionDisplayStats": {
@@ -416,7 +420,7 @@
 		"description": "Options page label: 'save the page of a newly created bookmark'"
 	},
 	"optionReplaceBookmarkURL": {
-		"message": "link the new bookmark to the saved page",
+		"message": "新しいブックマークを保存したページにリンクさせることを可能にする",
 		"description": "Options page label: 'link the new bookmark to the saved page'"
 	},
 	"optionsHelpLink": {
@@ -631,4 +635,4 @@
 		"message": "Cancel",
 		"description": "Add URLs popup cancel button: 'Cancel'"
 	}
-}
+}

+ 4 - 0
_locales/pl/messages.json

@@ -235,6 +235,10 @@
 		"message": "maksymalny czas bezczynności (ms)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "pomniejsz stronę",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "usuwaj obrazy dla alternatywnych rozdzielczości ekranu",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/ru/messages.json

@@ -235,6 +235,10 @@
 		"message": "максимальное время простоя (мс)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "удалить изображения для альтернативных разрешений экрана",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 4 - 0
_locales/uk/messages.json

@@ -235,6 +235,10 @@
 		"message": "максимальний час простою (мс)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "zoom out the page",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "видалити зображення для альтернативних розширень екрану ",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 5 - 1
_locales/zh_CN/messages.json

@@ -196,7 +196,7 @@
 		"description": "Value for 'name conflict resolution' option: 'prompt for a name'"
 	},
 	"optionFilenameConflictActionSkip": {
-        "message": "skip duplicate files",
+        "message": "跳过重复文件",
         "description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
     },
     "optionsHTMLContentSubTitle": {
@@ -235,6 +235,10 @@
 		"message": "最长空闲时间(毫秒)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "缩小页面",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "移除用于备选分辨率的图片",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

+ 5 - 1
_locales/zh_TW/messages.json

@@ -196,7 +196,7 @@
 		"description": "Value for 'name conflict resolution' option: 'prompt for a name'"
 	},
 	"optionFilenameConflictActionSkip": {
-        "message": "skip duplicate files",
+        "message": "跳過重複文件",
         "description": "Value for 'filename conflict resolution' option: 'skip duplicate files'"
     },
     "optionsHTMLContentSubTitle": {
@@ -235,6 +235,10 @@
 		"message": "最長空閒時間(毫秒)",
 		"description": "Options page label: 'maximum idle time (ms)'"
 	},
+	"optionLoadDeferredImagesKeepZoomLevel": {
+		"message": "縮小頁面",
+		"description": "Options page label: 'zoom out the page'"
+	},
 	"optionRemoveAlternativeImages": {
 		"message": "移除用於備選分辨率的圖片",
 		"description": "Options page label: 'remove images for alternative screen resolutions'"

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

@@ -207,7 +207,7 @@ singlefile.extension.core.bg.business = (() => {
 		const tabId = taskInfo.tab.id;
 		const taskId = taskInfo.id;
 		taskInfo.cancelled = true;
-		singlefile.extension.core.bg.tabs.sendMessage(tabId, { method: "content.cancelSave" });
+		singlefile.extension.core.bg.tabs.sendMessage(tabId, { method: "content.cancelSave", resetZoomLevel: taskInfo.options.loadDeferredImagesKeepZoomLevel });
 		if (taskInfo.cancel) {
 			taskInfo.cancel();
 		}

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

@@ -43,6 +43,7 @@ singlefile.extension.core.bg.config = (() => {
 		loadDeferredImagesMaxIdleTime: 1500,
 		loadDeferredImagesBlockCookies: false,
 		loadDeferredImagesBlockStorage: false,
+		loadDeferredImagesKeepZoomLevel: false,
 		filenameTemplate: "{page-title} ({date-iso} {time-locale}).html",
 		infobarTemplate: "",
 		includeInfobar: false,

+ 36 - 36
extension/core/bg/downloads.js

@@ -141,27 +141,12 @@ singlefile.extension.core.bg.downloads = (() => {
 				});
 			} else {
 				message.url = URL.createObjectURL(blob);
-				const filenameConflictAction = message.filenameConflictAction;
-				let skipped;
-				if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
-					const downloadItems = await browser.downloads.search({
-						filenameRegex: "(\\\\|/)" + getRegExp(message.filename) + "$",
-						exists: true
-					});
-					if (downloadItems.length) {
-						skipped = true;
-					} else {
-						message.filenameConflictAction = CONFLICT_ACTION_UNIQUIFY;
-					}
-				}
-				if (!skipped) {
-					await downloadPage(message, {
-						confirmFilename: message.confirmFilename,
-						incognito,
-						filenameConflictAction: message.filenameConflictAction,
-						filenameReplacementCharacter: message.filenameReplacementCharacter
-					});
-				}
+				await downloadPage(message, {
+					confirmFilename: message.confirmFilename,
+					incognito,
+					filenameConflictAction: message.filenameConflictAction,
+					filenameReplacementCharacter: message.filenameReplacementCharacter
+				});
 			}
 			singlefile.extension.ui.bg.main.onEnd(tabId);
 		} catch (error) {
@@ -238,24 +223,39 @@ singlefile.extension.core.bg.downloads = (() => {
 	}
 
 	async function downloadPage(pageData, options) {
-		const downloadInfo = {
-			url: pageData.url,
-			saveAs: options.confirmFilename,
-			filename: pageData.filename,
-			conflictAction: options.filenameConflictAction
-		};
-		if (options.incognito) {
-			downloadInfo.incognito = true;
+		const filenameConflictAction = options.filenameConflictAction;
+		let skipped;
+		if (filenameConflictAction == CONFLICT_ACTION_SKIP) {
+			const downloadItems = await browser.downloads.search({
+				filenameRegex: "(\\\\|/)" + getRegExp(pageData.filename) + "$",
+				exists: true
+			});
+			if (downloadItems.length) {
+				skipped = true;
+			} else {
+				options.filenameConflictAction = CONFLICT_ACTION_UNIQUIFY;
+			}
 		}
-		const downloadData = await download(downloadInfo, options.filenameReplacementCharacter);
-		if (downloadData.filename && pageData.bookmarkId && pageData.replaceBookmarkURL) {
-			if (!downloadData.filename.startsWith("file:")) {
-				if (downloadData.filename.startsWith("/")) {
-					downloadData.filename = downloadData.filename.substring(1);
+		if (!skipped) {
+			const downloadInfo = {
+				url: pageData.url,
+				saveAs: options.confirmFilename,
+				filename: pageData.filename,
+				conflictAction: options.filenameConflictAction
+			};
+			if (options.incognito) {
+				downloadInfo.incognito = true;
+			}
+			const downloadData = await download(downloadInfo, options.filenameReplacementCharacter);
+			if (downloadData.filename && pageData.bookmarkId && pageData.replaceBookmarkURL) {
+				if (!downloadData.filename.startsWith("file:")) {
+					if (downloadData.filename.startsWith("/")) {
+						downloadData.filename = downloadData.filename.substring(1);
+					}
+					downloadData.filename = "file:///" + downloadData.filename;
 				}
-				downloadData.filename = "file:///" + downloadData.filename;
+				await singlefile.extension.core.bg.bookmarks.update(pageData.bookmarkId, { url: downloadData.filename });
 			}
-			await singlefile.extension.core.bg.bookmarks.update(pageData.bookmarkId, { url: downloadData.filename });
 		}
 	}
 

+ 6 - 0
extension/core/content/content-main.js

@@ -56,6 +56,9 @@ this.singlefile.extension.core.content.main = this.singlefile.extension.core.con
 					ui.onEndPage();
 					browser.runtime.sendMessage({ method: "ui.processCancelled" });
 				}
+				if (message.resetZoomLevel) {
+					singlefile.lib.processors.lazy.content.loader.resetZoomLevel();
+				}
 				return {};
 			}
 			if (message.method == "content.getSelectedLinks") {
@@ -141,6 +144,9 @@ this.singlefile.extension.core.content.main = this.singlefile.extension.core.con
 			if (!processor.cancelled) {
 				if (event.type == event.RESOURCES_INITIALIZED) {
 					maxIndex = event.detail.max;
+					if (options.loadDeferredImagesKeepZoomLevel) {
+						singlefile.lib.processors.lazy.content.loader.resetZoomLevel();
+					}
 				}
 				if (event.type == event.RESOURCES_INITIALIZED || event.type == event.RESOURCE_LOADED) {
 					if (event.type == event.RESOURCE_LOADED) {

+ 6 - 0
extension/ui/bg/ui-options.js

@@ -42,6 +42,7 @@
 	const compressCSSLabel = document.getElementById("compressCSSLabel");
 	const loadDeferredImagesLabel = document.getElementById("loadDeferredImagesLabel");
 	const loadDeferredImagesMaxIdleTimeLabel = document.getElementById("loadDeferredImagesMaxIdleTimeLabel");
+	const loadDeferredImagesKeepZoomLevelLabel = document.getElementById("loadDeferredImagesKeepZoomLevelLabel");
 	const addMenuEntryLabel = document.getElementById("addMenuEntryLabel");
 	const filenameTemplateLabel = document.getElementById("filenameTemplateLabel");
 	const filenameMaxLengthLabel = document.getElementById("filenameMaxLengthLabel");
@@ -118,6 +119,7 @@
 	const compressCSSInput = document.getElementById("compressCSSInput");
 	const loadDeferredImagesInput = document.getElementById("loadDeferredImagesInput");
 	const loadDeferredImagesMaxIdleTimeInput = document.getElementById("loadDeferredImagesMaxIdleTimeInput");
+	const loadDeferredImagesKeepZoomLevelInput = document.getElementById("loadDeferredImagesKeepZoomLevelInput");
 	const contextMenuEnabledInput = document.getElementById("contextMenuEnabledInput");
 	const filenameTemplateInput = document.getElementById("filenameTemplateInput");
 	const filenameMaxLengthInput = document.getElementById("filenameMaxLengthInput");
@@ -437,6 +439,7 @@
 	compressCSSLabel.textContent = browser.i18n.getMessage("optionCompressCSS");
 	loadDeferredImagesLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImages");
 	loadDeferredImagesMaxIdleTimeLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesMaxIdleTime");
+	loadDeferredImagesKeepZoomLevelLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesKeepZoomLevel");
 	addMenuEntryLabel.textContent = browser.i18n.getMessage("optionAddMenuEntry");
 	filenameTemplateLabel.textContent = browser.i18n.getMessage("optionFilenameTemplate");
 	filenameMaxLengthLabel.textContent = browser.i18n.getMessage("optionFilenameMaxLength");
@@ -620,6 +623,8 @@
 		loadDeferredImagesInput.checked = profileOptions.loadDeferredImages && !profileOptions.saveRawPage;
 		loadDeferredImagesInput.disabled = profileOptions.saveRawPage;
 		loadDeferredImagesMaxIdleTimeInput.value = profileOptions.loadDeferredImagesMaxIdleTime;
+		loadDeferredImagesKeepZoomLevelInput.checked = profileOptions.loadDeferredImagesKeepZoomLevel && !profileOptions.saveRawPage;
+		loadDeferredImagesKeepZoomLevelInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPape;
 		loadDeferredImagesMaxIdleTimeInput.disabled = !profileOptions.loadDeferredImages || profileOptions.saveRawPage;
 		contextMenuEnabledInput.checked = profileOptions.contextMenuEnabled;
 		filenameTemplateInput.value = profileOptions.filenameTemplate;
@@ -696,6 +701,7 @@
 				compressCSS: compressCSSInput.checked,
 				loadDeferredImages: loadDeferredImagesInput.checked,
 				loadDeferredImagesMaxIdleTime: Math.max(loadDeferredImagesMaxIdleTimeInput.value, 0),
+				loadDeferredImagesKeepZoomLevel: loadDeferredImagesKeepZoomLevelInput.checked,
 				contextMenuEnabled: contextMenuEnabledInput.checked,
 				filenameTemplate: filenameTemplateInput.value,
 				filenameMaxLength: filenameMaxLengthInput.value,

+ 7 - 4
extension/ui/content/content-ui-main.js

@@ -58,8 +58,8 @@ this.singlefile.extension.ui.content.main = this.singlefile.extension.ui.content
 			let maskElement = document.querySelector(MASK_TAGNAME);
 			if (!maskElement) {
 				if (options.logsEnabled) {
-					document.body.appendChild(logsWindowElement);
 					setLogsWindowStyle();
+					document.body.appendChild(logsWindowElement);
 				}
 				if (options.shadowEnabled) {
 					const maskElement = createMaskElement();
@@ -378,10 +378,9 @@ this.singlefile.extension.ui.content.main = this.singlefile.extension.ui.content
 	function createMaskElement() {
 		let maskElement = document.querySelector(MASK_TAGNAME);
 		if (!maskElement) {
-			maskElement = createElement(MASK_TAGNAME, document.body);
+			maskElement = createElement(MASK_TAGNAME);
 			maskElement.style.setProperty("opacity", 0, "important");
 			maskElement.style.setProperty("background-color", "transparent", "important");
-			maskElement.offsetWidth;
 			maskElement.style.setProperty("position", "fixed", "important");
 			maskElement.style.setProperty("top", "0", "important");
 			maskElement.style.setProperty("left", "0", "important");
@@ -389,6 +388,8 @@ this.singlefile.extension.ui.content.main = this.singlefile.extension.ui.content
 			maskElement.style.setProperty("height", "100%", "important");
 			maskElement.style.setProperty("z-index", 2147483646, "important");
 			maskElement.style.setProperty("transition", "opacity 250ms", "important");
+			document.body.appendChild(maskElement);
+			maskElement.offsetWidth;
 		}
 		return maskElement;
 	}
@@ -529,7 +530,9 @@ this.singlefile.extension.ui.content.main = this.singlefile.extension.ui.content
 	function createElement(tagName, parentElement) {
 		const element = document.createElement(tagName);
 		element.className = SINGLE_FILE_UI_ELEMENT_CLASS;
-		parentElement.appendChild(element);
+		if (parentElement) {
+			parentElement.appendChild(element);
+		}
 		initStyle(element);
 		return element;
 	}

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

@@ -267,6 +267,13 @@
 							for example the network or system conditions are degraded. You can also decrease this value
 							otherwise.</p>
 					</li>
+					<li data-options-label="loadDeferredImagesKeepZoomLevelLabel"> <span class="option">Option: zoom out
+							the page</span>
+						<p>Check this option to save the entire content of a page by zooming out the page. This can help
+							saving pages using infinite virtual lists for example.
+						</p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					</li>
 					<li data-options-label="removeAlternativeImagesLabel"> <span class="option">Option: remove images
 							for alternative screen resolutions</span>
 						<p>Check this option to remove images that are alternatives in lower and/or higher resolutions

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

@@ -135,6 +135,10 @@
 				<label for="loadDeferredImagesMaxIdleTimeInput" id="loadDeferredImagesMaxIdleTimeLabel"></label>
 				<input type="number" class="large-input" id="loadDeferredImagesMaxIdleTimeInput" step="100">
 			</div>
+			<div class="option second-level">
+				<label for="loadDeferredImagesKeepZoomLevelInput" id="loadDeferredImagesKeepZoomLevelLabel"></label>
+				<input type="checkbox" id="loadDeferredImagesKeepZoomLevelInput">
+			</div>
 			<div class="option">
 				<label for="removeAlternativeImagesInput" id="removeAlternativeImagesLabel"></label>
 				<input type="checkbox" id="removeAlternativeImagesInput">

+ 8 - 2
lib/single-file/modules/css-fonts-alt-minifier.js

@@ -273,11 +273,17 @@ this.singlefile.lib.modules.fontsAltMinifier = this.singlefile.lib.modules.fonts
 		const cssTree = singlefile.lib.vendor.cssTree;
 		let property;
 		if (ruleData.block.children) {
-			property = ruleData.block.children.filter(node => node.property == propertyName).tail;
+			property = ruleData.block.children.filter(node => {
+				try {
+					return node.property == propertyName && !cssTree.generate(node.value).match(/\\9$/);
+				} catch (error) {
+					return node.property == propertyName;
+				}
+			}).tail;
 		}
 		if (property) {
 			try {
-				return cssTree.generate(property.data.value).replace(/\\9$/, "");
+				return cssTree.generate(property.data.value);
 			} catch (error) {
 				// ignored
 			}

+ 5 - 5
lib/single-file/modules/html-serializer.js

@@ -77,13 +77,13 @@ this.singlefile.lib.modules.serializer = this.singlefile.lib.modules.serializer
 		}
 	};
 
-	function serialize(node, compressHTML) {
+	function serialize(node, compressHTML, isSVG) {
 		if (node.nodeType == Node_TEXT_NODE) {
 			return serializeTextNode(node);
 		} else if (node.nodeType == Node_COMMENT_NODE) {
 			return serializeCommentNode(node);
 		} else if (node.nodeType == Node_ELEMENT_NODE) {
-			return serializeElement(node, compressHTML);
+			return serializeElement(node, compressHTML, isSVG);
 		}
 	}
 
@@ -107,7 +107,7 @@ this.singlefile.lib.modules.serializer = this.singlefile.lib.modules.serializer
 		return "<!--" + commentNode.textContent + "-->";
 	}
 
-	function serializeElement(element, compressHTML) {
+	function serializeElement(element, compressHTML, isSVG) {
 		const tagName = element.tagName.toLowerCase();
 		const omittedStartTag = compressHTML && OMITTED_START_TAGS.find(omittedStartTag => tagName == omittedStartTag.tagName && omittedStartTag.accept(element));
 		let content = "";
@@ -119,10 +119,10 @@ this.singlefile.lib.modules.serializer = this.singlefile.lib.modules.serializer
 		if (element.tagName == "TEMPLATE" && !element.childNodes.length) {
 			content += element.innerHTML;
 		} else {
-			Array.from(element.childNodes).forEach(childNode => content += serialize(childNode, compressHTML));
+			Array.from(element.childNodes).forEach(childNode => content += serialize(childNode, compressHTML, isSVG || tagName == "svg"));
 		}
 		const omittedEndTag = compressHTML && OMITTED_END_TAGS.find(omittedEndTag => tagName == omittedEndTag.tagName && omittedEndTag.accept(element.nextSibling, element));
-		if (!omittedEndTag && !SELF_CLOSED_TAG_NAMES.includes(tagName)) {
+		if (isSVG || (!omittedEndTag && !SELF_CLOSED_TAG_NAMES.includes(tagName))) {
 			content += "</" + tagName + ">";
 		}
 		return content;

+ 95 - 57
lib/single-file/processors/hooks/content/content-hooks-frames-web.js

@@ -27,6 +27,9 @@
 
 	const LOAD_DEFERRED_IMAGES_START_EVENT = "single-file-load-deferred-images-start";
 	const LOAD_DEFERRED_IMAGES_END_EVENT = "single-file-load-deferred-images-end";
+	const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT = "single-file-load-deferred-images-keep-zoom-level-start";
+	const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-end";
+	const LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-reset";
 	const BLOCK_COOKIES_START_EVENT = "single-file-block-cookies-start";
 	const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
 	const BLOCK_STORAGE_START_EVENT = "single-file-block-storage-start";
@@ -60,36 +63,41 @@
 	const observers = new Map();
 	const observedElements = new Map();
 
-	addEventListener.call(window, LOAD_DEFERRED_IMAGES_START_EVENT, () => {
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_START_EVENT, () => loadDeferredImagesStart());
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT, () => loadDeferredImagesStart(true));
+
+	function loadDeferredImagesStart(keepZoomLevel) {
 		const scrollingElement = document.scrollingElement || document.documentElement;
 		const clientHeight = scrollingElement.clientHeight;
 		const clientWidth = scrollingElement.clientWidth;
 		const scrollHeight = Math.max(scrollingElement.scrollHeight - clientHeight, clientHeight);
 		const scrollWidth = Math.max(scrollingElement.scrollWidth - clientWidth, clientWidth);
-		scrollingElement.__defineGetter__("clientHeight", () => scrollHeight);
-		scrollingElement.__defineGetter__("clientWidth", () => scrollWidth);
-		screen.__defineGetter__("height", () => scrollHeight);
-		screen.__defineGetter__("width", () => scrollWidth);
 		document.querySelectorAll("[loading=lazy]").forEach(element => {
 			element.loading = "eager";
 			element.setAttribute(LAZY_LOAD_ATTRIBUTE, "");
 		});
-		if (!window._singleFile_getBoundingClientRect) {
-			window._singleFile_getBoundingClientRect = Element.prototype.getBoundingClientRect;
-			Element.prototype.getBoundingClientRect = function () {
-				const boundingRect = window._singleFile_getBoundingClientRect.call(this);
-				if (this == scrollingElement) {
-					boundingRect.__defineGetter__("height", () => scrollHeight);
-					boundingRect.__defineGetter__("bottom", () => scrollHeight + boundingRect.top);
-					boundingRect.__defineGetter__("width", () => scrollWidth);
-					boundingRect.__defineGetter__("right", () => scrollWidth + boundingRect.left);
-				}
-				return boundingRect;
-			};
-			window._singleFile_innerHeight = window.innerHeight;
-			window._singleFile_innerWidth = window.innerWidth;
-			window.__defineGetter__("innerHeight", () => scrollHeight);
-			window.__defineGetter__("innerWidth", () => scrollWidth);
+		if (!keepZoomLevel) {
+			scrollingElement.__defineGetter__("clientHeight", () => scrollHeight);
+			scrollingElement.__defineGetter__("clientWidth", () => scrollWidth);
+			screen.__defineGetter__("height", () => scrollHeight);
+			screen.__defineGetter__("width", () => scrollWidth);
+			if (!window._singleFile_getBoundingClientRect) {
+				window._singleFile_getBoundingClientRect = Element.prototype.getBoundingClientRect;
+				Element.prototype.getBoundingClientRect = function () {
+					const boundingRect = window._singleFile_getBoundingClientRect.call(this);
+					if (this == scrollingElement) {
+						boundingRect.__defineGetter__("height", () => scrollHeight);
+						boundingRect.__defineGetter__("bottom", () => scrollHeight + boundingRect.top);
+						boundingRect.__defineGetter__("width", () => scrollWidth);
+						boundingRect.__defineGetter__("right", () => scrollWidth + boundingRect.left);
+					}
+					return boundingRect;
+				};
+				window._singleFile_innerHeight = window.innerHeight;
+				window._singleFile_innerWidth = window.innerWidth;
+				window.__defineGetter__("innerHeight", () => scrollHeight);
+				window.__defineGetter__("innerWidth", () => scrollWidth);
+			}
 		}
 		if (!window._singleFileImage) {
 			const Image = window.Image;
@@ -123,8 +131,14 @@
 				};
 			});
 		}
-		const zoomFactorX = (clientHeight + window.scrollY) / scrollHeight;
-		const zoomFactorY = (clientWidth + window.scrollX) / scrollWidth;
+		let zoomFactorX, zoomFactorY;
+		if (keepZoomLevel) {
+			zoomFactorX = clientHeight / scrollHeight;
+			zoomFactorY = clientWidth / scrollWidth;
+		} else {
+			zoomFactorX = (clientHeight + window.scrollY) / scrollHeight;
+			zoomFactorY = (clientWidth + window.scrollX) / scrollWidth;
+		}
 		const zoomFactor = Math.min(zoomFactorX, zoomFactorY);
 		if (zoomFactor < 1) {
 			const transform = document.documentElement.style.getPropertyValue("transform");
@@ -135,54 +149,78 @@
 			document.documentElement.style.setProperty("transform", "scale3d(" + zoomFactor + ", " + zoomFactor + ", 1)", "important");
 			dispatchEvent.call(window, new UIEvent("resize"));
 			dispatchEvent.call(window, new UIEvent("scroll"));
-			document.documentElement.style.setProperty("transform", transform, transformPriority);
-			document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
-		}
-		dispatchEvent.call(window, new UIEvent("resize"));
-		dispatchEvent.call(window, new UIEvent("scroll"));
-		const docBoundingRect = scrollingElement.getBoundingClientRect();
-		[...observers].forEach(([intersectionObserver, observer]) => {
-			const rootBoundingRect = observer.options && observer.options.root && observer.options.root.getBoundingClientRect();
-			const targetElements = observedElements.get(intersectionObserver);
-			if (targetElements) {
-				observer.callback(targetElements.map(target => {
-					const boundingClientRect = target.getBoundingClientRect();
-					const isIntersecting = true;
-					const intersectionRatio = 1;
-					const rootBounds = observer.options && observer.options.root ? rootBoundingRect : docBoundingRect;
-					const time = 0;
-					return { target, intersectionRatio, boundingClientRect, intersectionRect: boundingClientRect, isIntersecting, rootBounds, time };
-				}), intersectionObserver);
+			if (keepZoomLevel) {
+				document.documentElement.style.setProperty("-sf-transform", transform, transformPriority);
+				document.documentElement.style.setProperty("-sf-transform-origin", transformOrigin, transformOriginPriority);
+			} else {
+				document.documentElement.style.setProperty("transform", transform, transformPriority);
+				document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
 			}
-		});
+		}
+		if (!keepZoomLevel) {
+			dispatchEvent.call(window, new UIEvent("resize"));
+			dispatchEvent.call(window, new UIEvent("scroll"));
+			const docBoundingRect = scrollingElement.getBoundingClientRect();
+			[...observers].forEach(([intersectionObserver, observer]) => {
+				const rootBoundingRect = observer.options && observer.options.root && observer.options.root.getBoundingClientRect();
+				const targetElements = observedElements.get(intersectionObserver);
+				if (targetElements) {
+					observer.callback(targetElements.map(target => {
+						const boundingClientRect = target.getBoundingClientRect();
+						const isIntersecting = true;
+						const intersectionRatio = 1;
+						const rootBounds = observer.options && observer.options.root ? rootBoundingRect : docBoundingRect;
+						const time = 0;
+						return { target, intersectionRatio, boundingClientRect, intersectionRect: boundingClientRect, isIntersecting, rootBounds, time };
+					}), intersectionObserver);
+				}
+			});
+		}
+	}
+
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_END_EVENT, () => loadDeferredImagesEnd());
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT, () => loadDeferredImagesEnd(true));
+	addEventListener.call(window, LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_END_EVENT, () => {
+		const transform = document.documentElement.style.getPropertyValue("-sf-transform");
+		const transformPriority = document.documentElement.style.getPropertyPriority("-sf-transform");
+		const transformOrigin = document.documentElement.style.getPropertyValue("-sf-transform-origin");
+		const transformOriginPriority = document.documentElement.style.getPropertyPriority("-sf-transform-origin");
+		document.documentElement.style.setProperty("transform", transform, transformPriority);
+		document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
+		document.documentElement.style.removeProperty("-sf-transform");
+		document.documentElement.style.removeProperty("-sf-transform-origin");
 	});
 
-	addEventListener.call(window, LOAD_DEFERRED_IMAGES_END_EVENT, () => {
+	function loadDeferredImagesEnd(keepZoomLevel) {
 		const scrollingElement = document.scrollingElement || document.documentElement;
 		document.querySelectorAll("[" + LAZY_LOAD_ATTRIBUTE + "]").forEach(element => {
 			element.loading = "lazy";
 			element.removeAttribute(LAZY_LOAD_ATTRIBUTE);
 		});
-		delete scrollingElement.clientHeight;
-		delete scrollingElement.clientWidth;
-		delete screen.height;
-		delete screen.width;
-		if (window._singleFile_getBoundingClientRect) {
-			Element.prototype.getBoundingClientRect = window._singleFile_getBoundingClientRect;
-			window.innerHeight = window._singleFile_innerHeight;
-			window.innerWidth = window._singleFile_innerWidth;
-			delete window._singleFile_getBoundingClientRect;
-			delete window._singleFile_innerHeight;
-			delete window._singleFile_innerWidth;
+		if (!keepZoomLevel) {
+			delete scrollingElement.clientHeight;
+			delete scrollingElement.clientWidth;
+			delete screen.height;
+			delete screen.width;
+			if (window._singleFile_getBoundingClientRect) {
+				Element.prototype.getBoundingClientRect = window._singleFile_getBoundingClientRect;
+				window.innerHeight = window._singleFile_innerHeight;
+				window.innerWidth = window._singleFile_innerWidth;
+				delete window._singleFile_getBoundingClientRect;
+				delete window._singleFile_innerHeight;
+				delete window._singleFile_innerWidth;
+			}
 		}
 		if (window._singleFileImage) {
 			delete window.Image;
 			window.Image = window._singleFileImage;
 			delete window._singleFileImage;
 		}
-		dispatchEvent.call(window, new UIEvent("resize"));
-		dispatchEvent.call(window, new UIEvent("scroll"));
-	});
+		if (!keepZoomLevel) {
+			dispatchEvent.call(window, new UIEvent("resize"));
+			dispatchEvent.call(window, new UIEvent("scroll"));
+		}
+	}
 
 	addEventListener.call(window, BLOCK_COOKIES_START_EVENT, () => {
 		try {

+ 16 - 2
lib/single-file/processors/hooks/content/content-hooks-frames.js

@@ -27,6 +27,9 @@ this.singlefile.lib.processors.hooks.content.frames = this.singlefile.lib.proces
 
 	const LOAD_DEFERRED_IMAGES_START_EVENT = "single-file-load-deferred-images-start";
 	const LOAD_DEFERRED_IMAGES_END_EVENT = "single-file-load-deferred-images-end";
+	const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT = "single-file-load-deferred-images-keep-zoom-level-start";
+	const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-end";
+	const LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-reset";
 	const BLOCK_COOKIES_START_EVENT = "single-file-block-cookies-start";
 	const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
 	const BLOCK_STORAGE_START_EVENT = "single-file-block-storage-start";
@@ -80,7 +83,11 @@ this.singlefile.lib.processors.hooks.content.frames = this.singlefile.lib.proces
 			if (options.loadDeferredImagesBlockStorage) {
 				dispatchEvent.call(window, new CustomEvent(BLOCK_STORAGE_START_EVENT));
 			}
-			dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_START_EVENT));
+			if (options.loadDeferredImagesKeepZoomLevel) {
+				dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT));
+			} else {
+				dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_START_EVENT));
+			}
 		},
 		loadDeferredImagesEnd: options => {
 			if (options.loadDeferredImagesBlockCookies) {
@@ -89,7 +96,14 @@ this.singlefile.lib.processors.hooks.content.frames = this.singlefile.lib.proces
 			if (options.loadDeferredImagesBlockStorage) {
 				dispatchEvent.call(window, new CustomEvent(BLOCK_STORAGE_END_EVENT));
 			}
-			dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_END_EVENT));
+			if (options.loadDeferredImagesKeepZoomLevel) {
+				dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT));
+			} else {
+				dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_END_EVENT));
+			}
+		},
+		loadDeferredImagesResetZoomLevel: () => {
+			dispatchEvent.call(window, new CustomEvent(LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_END_EVENT));
 		},
 		LOAD_IMAGE_EVENT,
 		IMAGE_LOADED_EVENT

+ 6 - 0
lib/single-file/processors/lazy/content/content-lazy-loader.js

@@ -44,6 +44,12 @@ this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.process
 			if (scrollY <= maxScrollY && scrollX <= maxScrollX) {
 				return process(options);
 			}
+		},
+		resetZoomLevel: () => {
+			const frames = singlefile.lib.processors.hooks.content.frames;
+			if (frames) {
+				frames.loadDeferredImagesResetZoomLevel();
+			}
 		}
 	};
 

+ 14 - 2
lib/single-file/single-file-core.js

@@ -93,6 +93,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 	const STAGES = [{
 		sequential: [
 			{ action: "preProcessPage" },
+			{ option: "loadDeferredImagesKeepZoomLevel", action: "resetZoomLevel" },
 			{ action: "replaceStyleContents" },
 			{ action: "resetCharsetMeta" },
 			{ option: "saveFavicon", action: "saveFavicon" },
@@ -1351,6 +1352,17 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 			}
 		}
 
+		resetZoomLevel() {
+			const transform = this.doc.documentElement.style.getPropertyValue("-sf-transform");
+			const transformPriority = this.doc.documentElement.style.getPropertyPriority("-sf-transform");
+			const transformOrigin = this.doc.documentElement.style.getPropertyValue("-sf-transform-origin");
+			const transformOriginPriority = this.doc.documentElement.style.getPropertyPriority("-sf-transform-origin");
+			this.doc.documentElement.style.setProperty("transform", transform, transformPriority);
+			this.doc.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
+			this.doc.documentElement.style.removeProperty("-sf-transform");
+			this.doc.documentElement.style.removeProperty("-sf-transform-origin");
+		}
+
 		async insertMAFFMetaData() {
 			const maffMetaData = await this.maffMetaDataPromise;
 			if (maffMetaData && maffMetaData.content) {
@@ -1526,7 +1538,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 								maxResourceSize: options.maxResourceSize,
 								maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 								validateTextContentType: true,
-								frameId: options.windowId
+								frameId: options.frameId
 							});
 							resourceURL = content.resourceURL;
 							content.data = getUpdatedResourceContent(resourceURL, content, options);
@@ -1596,7 +1608,7 @@ this.singlefile.lib.core = this.singlefile.lib.core || (() => {
 					maxResourceSize: options.maxResourceSize,
 					maxResourceSizeEnabled: options.maxResourceSizeEnabled,
 					charset: options.charset,
-					frameId: options.windowId,
+					frameId: options.frameId,
 					validateTextContentType: true
 				});
 				resourceURL = content.resourceURL;

+ 1 - 1
manifest.json

@@ -8,7 +8,7 @@
 		"64": "extension/ui/resources/icon_64.png",
 		"128": "extension/ui/resources/icon_128.png"
 	},
-	"version": "1.17.44",
+	"version": "1.17.52",
 	"description": "__MSG_extensionDescription__",
 	"content_scripts": [
 		{