Prechádzať zdrojové kódy

add option "move in the head element the styles found outside of it"
(fix #841)

Gildas 4 rokov pred
rodič
commit
152110b950

+ 4 - 0
_locales/de/messages.json

@@ -279,6 +279,10 @@
 		"message": "CSS-Inhalte komprimieren",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "Die Stile, die sich außerhalb des Head-Elements befinden, in dieses verschieben",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "Nicht verwendete Stile entfernen",
 		"description": "Options page label: 'remove unused styles'"

+ 5 - 1
_locales/en/messages.json

@@ -166,7 +166,7 @@
 	"optionSaveOriginalURLs": {
 		"message": "save original URLs of embedded resources",
 		"description": "Options page label: 'save original URLs of embedded resources'"
-	},	
+	},
 	"optionIncludeInfobar": {
 		"message": "include the infobar in the saved page",
 		"description": "Options page label: 'include the infobar in the saved page'"
@@ -279,6 +279,10 @@
 		"message": "compress CSS content",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "move in the head element the styles found outside of it",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "remove unused styles",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/es/messages.json

@@ -279,6 +279,10 @@
 		"message": "comprimir contenido CSS",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "mover al elemento de head los estilos que se encuentran fuera de él",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "eliminar estilos no usados",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/fr/messages.json

@@ -279,6 +279,10 @@
 		"message": "compresser le contenu CSS",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "déplacer dans l'élément head les styles trouvés en dehors de celui-ci",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "supprimer les styles inutilisés",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/it/messages.json

@@ -279,6 +279,10 @@
 		"message": "comprimi contenuto CSS",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "move in the head element the styles found outside of it",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "Rimuovi stili non utilizzati",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/ja/messages.json

@@ -279,6 +279,10 @@
 		"message": "CSS コンテンツを圧縮する",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "move in the head element the styles found outside of it",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "未使用のスタイルを削除する",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/pl/messages.json

@@ -279,6 +279,10 @@
 		"message": "kompresuj zawartość CSS",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "move in the head element the styles found outside of it",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "usuwaj nieużywane style",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/ru/messages.json

@@ -279,6 +279,10 @@
 		"message": "сжать содержимое CSS",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "move in the head element the styles found outside of it",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "удалить неиспользуемые стили",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/uk/messages.json

@@ -279,6 +279,10 @@
 		"message": "стискати вміст CSS ",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "move in the head element the styles found outside of it",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "видалити невикористовувані стилі",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/zh_CN/messages.json

@@ -279,6 +279,10 @@
 		"message": "压缩 CSS 内容",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "move in the head element the styles found outside of it",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "移除未使用的样式",
 		"description": "Options page label: 'remove unused styles'"

+ 4 - 0
_locales/zh_TW/messages.json

@@ -279,6 +279,10 @@
 		"message": "壓縮 CSS 內容",
 		"description": "Options page label: 'compress CSS content'"
 	},
+	"optionMoveStylesInHead": {
+		"message": "move in the head element the styles found outside of it",
+		"description": "Options page label: 'move in the head element the styles found outside of it'"
+	},
 	"optionRemoveUnusedStyles": {
 		"message": "移除未使用的樣式",
 		"description": "Options page label: 'remove unused styles'"

+ 18 - 11
cli/args.js

@@ -30,7 +30,15 @@ const args = require("yargs")
 		yargs.positional("output", { description: "Output filename", type: "string" });
 	})
 	.default({
+		"accept-headers": {
+			"font": "application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8",
+			"image": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
+			"stylesheet": "text/css,*/*;q=0.1",
+			"script": "*/*",
+			"document": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
+		},
 		"back-end": "puppeteer",
+		"block-mixed-content": false,
 		"browser-server": "",
 		"browser-headless": true,
 		"browser-executable-path": "",
@@ -64,6 +72,8 @@ const args = require("yargs")
 		"max-parallel-workers": 8,
 		"max-resource-size-enabled": false,
 		"max-resource-size": 10,
+		"move-styles-in-head": false,
+		"output-directory": "",
 		"remove-hidden-elements": true,
 		"remove-unused-styles": true,
 		"remove-unused-fonts": true,
@@ -75,6 +85,7 @@ const args = require("yargs")
 		"remove-alternative-fonts": true,
 		"remove-alternative-medias": true,
 		"remove-alternative-images": true,
+		"save-original-urls": false,
 		"save-raw-page": false,
 		"web-driver-executable-path": "",
 		"user-script-enabled": true,
@@ -85,20 +96,12 @@ const args = require("yargs")
 		"crawl-max-depth": 1,
 		"crawl-external-links-max-depth": 1,
 		"crawl-replace-urls": false,
-		"crawl-rewrite-rule": [],
-		"output-directory": "",
-		"blockMixedContent": false,
-		"saveOriginalURLs": false,
-		"acceptHeaders": {
-			"font": "application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8",
-			"image": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
-			"stylesheet": "text/css,*/*;q=0.1",
-			"script": "*/*",
-			"document": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
-		}
+		"crawl-rewrite-rule": []		
 	})
 	.options("back-end", { description: "Back-end to use" })
 	.choices("back-end", ["jsdom", "puppeteer", "webdriver-chromium", "webdriver-gecko", "puppeteer-firefox", "playwright-firefox", "playwright-chromium"])
+	.options("block-mixed-content", { description: "Block mixed contents" })
+	.boolean("block-mixed-content")
 	.options("browser-server", { description: "Server to connect to (puppeteer only for now)" })
 	.string("browser-server")
 	.options("browser-headless", { description: "Run the browser in headless mode (puppeteer, webdriver-gecko, webdriver-chromium)" })
@@ -191,6 +194,8 @@ const args = require("yargs")
 	.boolean("max-resource-size-enabled")
 	.options("max-resource-size", { description: "Maximum size of embedded resources in MB (i.e. images, stylesheets, scripts and iframes)" })
 	.number("max-resource-size")
+	.options("move-styles-in-head", { description: "Move style elements outside the head element into the head element" })
+	.boolean("move-styles-in-head")
 	.options("remove-frames", { description: "Remove frames (puppeteer, webdriver-gecko, webdriver-chromium)" })
 	.boolean("remove-frames")
 	.options("remove-hidden-elements", { description: "Remove HTML elements which are not displayed" })
@@ -213,6 +218,8 @@ const args = require("yargs")
 	.boolean("remove-alternative-medias")
 	.options("remove-alternative-images", { description: "Remove images for alternative sizes of screen" })
 	.boolean("remove-alternative-images")
+	.options("save-original-urls", { description: "Save the original URLS in the embedded contents" })
+	.boolean("save-original-urls")
 	.options("save-raw-page", { description: "Save the original page without interpreting it into the browser (puppeteer, webdriver-gecko, webdriver-chromium)" })
 	.boolean("save-raw-page")
 	.options("urls-file", { description: "Path to a text file containing a list of URLs (separated by a newline) to save" })

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

@@ -121,6 +121,7 @@ const DEFAULT_CONFIG = {
 		script: "*/*",
 		document: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
 	},
+	moveStylesInHead: false,
 	woleetKey: ""
 };
 

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

@@ -48,6 +48,7 @@ const githubBranchLabel = document.getElementById("githubBranchLabel");
 const saveWithCompanionLabel = document.getElementById("saveWithCompanionLabel");
 const compressHTMLLabel = document.getElementById("compressHTMLLabel");
 const compressCSSLabel = document.getElementById("compressCSSLabel");
+const moveStylesInHeadLabel = document.getElementById("moveStylesInHeadLabel");
 const loadDeferredImagesLabel = document.getElementById("loadDeferredImagesLabel");
 const loadDeferredImagesMaxIdleTimeLabel = document.getElementById("loadDeferredImagesMaxIdleTimeLabel");
 const loadDeferredImagesKeepZoomLevelLabel = document.getElementById("loadDeferredImagesKeepZoomLevelLabel");
@@ -148,6 +149,7 @@ const saveWithCompanionInput = document.getElementById("saveWithCompanionInput")
 const saveToFilesystemInput = document.getElementById("saveToFilesystemInput");
 const compressHTMLInput = document.getElementById("compressHTMLInput");
 const compressCSSInput = document.getElementById("compressCSSInput");
+const moveStylesInHeadInput = document.getElementById("moveStylesInHeadInput");
 const loadDeferredImagesInput = document.getElementById("loadDeferredImagesInput");
 const loadDeferredImagesMaxIdleTimeInput = document.getElementById("loadDeferredImagesMaxIdleTimeInput");
 const loadDeferredImagesKeepZoomLevelInput = document.getElementById("loadDeferredImagesKeepZoomLevelInput");
@@ -495,6 +497,7 @@ githubBranchLabel.textContent = browser.i18n.getMessage("optionGitHubBranch");
 saveWithCompanionLabel.textContent = browser.i18n.getMessage("optionSaveWithCompanion");
 compressHTMLLabel.textContent = browser.i18n.getMessage("optionCompressHTML");
 compressCSSLabel.textContent = browser.i18n.getMessage("optionCompressCSS");
+moveStylesInHeadLabel.textContent = browser.i18n.getMessage("optionMoveStylesInHead");
 loadDeferredImagesLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImages");
 loadDeferredImagesMaxIdleTimeLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesMaxIdleTime");
 loadDeferredImagesKeepZoomLevelLabel.textContent = browser.i18n.getMessage("optionLoadDeferredImagesKeepZoomLevel");
@@ -713,6 +716,7 @@ async function refresh(profileName) {
 	saveToFilesystemInput.checked = !profileOptions.saveToGDrive && !profileOptions.saveToGitHub && !profileOptions.saveWithCompanion && !saveToClipboardInput.checked;
 	compressHTMLInput.checked = profileOptions.compressHTML;
 	compressCSSInput.checked = profileOptions.compressCSS;
+	moveStylesInHeadInput.checked = profileOptions.moveStylesInHead;
 	loadDeferredImagesInput.checked = profileOptions.loadDeferredImages;
 	loadDeferredImagesMaxIdleTimeInput.value = profileOptions.loadDeferredImagesMaxIdleTime;
 	loadDeferredImagesKeepZoomLevelInput.checked = profileOptions.loadDeferredImagesKeepZoomLevel;
@@ -805,6 +809,7 @@ async function update() {
 			saveWithCompanion: saveWithCompanionInput.checked,
 			compressHTML: compressHTMLInput.checked,
 			compressCSS: compressCSSInput.checked,
+			moveStylesInHead: moveStylesInHeadInput.checked,
 			loadDeferredImages: loadDeferredImagesInput.checked,
 			loadDeferredImagesMaxIdleTime: Math.max(loadDeferredImagesMaxIdleTimeInput.value, 0),
 			loadDeferredImagesKeepZoomLevel: loadDeferredImagesKeepZoomLevelInput.checked,

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

@@ -268,6 +268,14 @@
 							to save a page.</p>
 						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
 					</li>
+					<li data-options-label="moveStylesInHeadLabel"> <span class="option">Option: move in the head
+							element the styles found outside of it</span>
+						<p>
+							Check this option to move all the style elements found outside the head element into the
+							head element. This can be useful to avoid a FOUC ("flash of unstyled content") when
+							displaying a saved page.</p>
+						<p class="notice">It is recommended to <u>uncheck</u> this option</p>
+					</li>
 				</ul>
 				<p>Images</p>
 				<ul>

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

@@ -133,6 +133,10 @@
 				<label for="compressCSSInput" id="compressCSSLabel"></label>
 				<input type="checkbox" id="compressCSSInput">
 			</div>
+			<div class="option">
+				<label for="moveStylesInHeadInput" id="moveStylesInHeadLabel"></label>
+				<input type="checkbox" id="moveStylesInHeadInput">
+			</div>
 		</details>
 		<details>
 			<summary id="imagesLabel"></summary>

+ 9 - 0
lib/single-file/single-file-core.js

@@ -101,6 +101,7 @@ const STAGES = [{
 		{ action: "insertFonts" },
 		{ action: "insertShadowRootContents" },
 		{ action: "setInputValues" },
+		{ option: "moveStylesInHead", action: "moveStylesInHead" },
 		{ option: "removeScripts", action: "removeScripts" },
 		{ option: "selected", action: "removeUnselectedElements" },
 		{ option: "removeVideoSrc", action: "insertVideoPosters" },
@@ -807,6 +808,14 @@ class Processor {
 		});
 	}
 
+	moveStylesInHead() {
+		this.doc.querySelectorAll("style").forEach(stylesheet => {
+			if (stylesheet.getAttribute(util.STYLE_ATTRIBUTE_NAME) == "") {
+				this.doc.head.appendChild(stylesheet);
+			}
+		});
+	}
+
 	saveFavicon() {
 		let faviconElement = this.doc.querySelector("link[href][rel=\"icon\"]");
 		if (!faviconElement) {

+ 16 - 0
lib/single-file/single-file-helper.js

@@ -39,6 +39,7 @@ const IMAGE_ATTRIBUTE_NAME = "data-single-file-image";
 const POSTER_ATTRIBUTE_NAME = "data-single-file-poster";
 const CANVAS_ATTRIBUTE_NAME = "data-single-file-canvas";
 const HTML_IMPORT_ATTRIBUTE_NAME = "data-single-file-import";
+const STYLE_ATTRIBUTE_NAME = "data-single-file-movable-style";
 const INPUT_VALUE_ATTRIBUTE_NAME = "data-single-file-input-value";
 const LAZY_SRC_ATTRIBUTE_NAME = "data-single-file-lazy-loaded-src";
 const STYLESHEET_ATTRIBUTE_NAME = "data-single-file-stylesheet";
@@ -86,6 +87,7 @@ export {
 	INPUT_VALUE_ATTRIBUTE_NAME,
 	SHADOW_ROOT_ATTRIBUTE_NAME,
 	HTML_IMPORT_ATTRIBUTE_NAME,
+	STYLE_ATTRIBUTE_NAME,
 	LAZY_SRC_ATTRIBUTE_NAME,
 	STYLESHEET_ATTRIBUTE_NAME,
 	SELECTED_CONTENT_ATTRIBUTE_NAME,
@@ -132,6 +134,15 @@ function preProcessDoc(doc, win, options) {
 	let elementsInfo;
 	if (win && doc.documentElement) {
 		elementsInfo = getElementsInfo(win, doc, doc.documentElement, options);
+		if (options.moveStylesInHead) {
+			doc.querySelectorAll("body style, body ~ style").forEach(element => {
+				const computedStyle = win.getComputedStyle(element);
+				if (computedStyle && testHiddenElement(element, computedStyle)) {
+					element.setAttribute(STYLE_ATTRIBUTE_NAME, "");
+					elementsInfo.markedElements.push(element);
+				}
+			});
+		}
 	} else {
 		elementsInfo = {
 			canvases: [],
@@ -397,6 +408,7 @@ function postProcessDoc(doc, markedElements) {
 		element.removeAttribute(HTML_IMPORT_ATTRIBUTE_NAME);
 		element.removeAttribute(STYLESHEET_ATTRIBUTE_NAME);
 		element.removeAttribute(ASYNC_SCRIPT_ATTRIBUTE_NAME);
+		element.removeAttribute(STYLE_ATTRIBUTE_NAME);
 	});
 }
 
@@ -426,6 +438,7 @@ function getSize(win, imageElement, computedStyle) {
 	let pxWidth = imageElement.naturalWidth;
 	let pxHeight = imageElement.naturalHeight;
 	if (!pxWidth && !pxHeight) {
+		const noStyleAttribute = imageElement.getAttribute("style") == null;
 		computedStyle = computedStyle || win.getComputedStyle(imageElement);
 		let removeBorderWidth = false;
 		if (computedStyle.getPropertyValue("box-sizing") == "content-box") {
@@ -455,6 +468,9 @@ function getSize(win, imageElement, computedStyle) {
 		}
 		pxWidth = Math.max(0, imageElement.clientWidth - paddingLeft - paddingRight - borderLeft - borderRight);
 		pxHeight = Math.max(0, imageElement.clientHeight - paddingTop - paddingBottom - borderTop - borderBottom);
+		if (noStyleAttribute) {
+			imageElement.removeAttribute("style");
+		}
 	}
 	return { pxWidth, pxHeight };
 }

+ 1 - 0
lib/single-file/single-file-util.js

@@ -176,6 +176,7 @@ function getInstance(utilOptions) {
 		POSTER_ATTRIBUTE_NAME: helper.POSTER_ATTRIBUTE_NAME,
 		CANVAS_ATTRIBUTE_NAME: helper.CANVAS_ATTRIBUTE_NAME,
 		HTML_IMPORT_ATTRIBUTE_NAME: helper.HTML_IMPORT_ATTRIBUTE_NAME,
+		STYLE_ATTRIBUTE_NAME: helper.STYLE_ATTRIBUTE_NAME,
 		INPUT_VALUE_ATTRIBUTE_NAME: helper.INPUT_VALUE_ATTRIBUTE_NAME,
 		SHADOW_ROOT_ATTRIBUTE_NAME: helper.SHADOW_ROOT_ATTRIBUTE_NAME,
 		PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME: helper.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME,