Browse Source

use modules in the library code of SingleFile

Gildas 5 years ago
parent
commit
eb2fc5c8d4
52 changed files with 6752 additions and 6706 deletions
  1. 5 0
      .eslintrc.js
  2. 3 3
      build-extension.sh
  3. 2 26
      cli/back-ends/common/scripts.js
  4. 1 1
      cli/back-ends/jsdom.js
  5. 1 1
      cli/back-ends/playwright-chromium.js
  6. 1 1
      cli/back-ends/playwright-firefox.js
  7. 1 1
      cli/back-ends/puppeteer-firefox.js
  8. 1 1
      cli/back-ends/puppeteer.js
  9. 1 1
      cli/back-ends/webdriver-chromium.js
  10. 1 1
      cli/back-ends/webdriver-gecko.js
  11. 2 2
      common/ui/content/content-infobar.js
  12. 17 17
      extension/core/content/content-bootstrap.js
  13. 7 7
      extension/core/content/content-main.js
  14. 1 1
      extension/index.js
  15. 2 26
      extension/lib/single-file/core/bg/scripts.js
  16. 1 1
      extension/ui/content/content-ui-editor-web.js
  17. 2 2
      extension/ui/content/content-ui-main.js
  18. 5 4
      extension/ui/pages/editor.html
  19. 3 0
      lib/single-file/build-lib.sh
  20. 0 0
      lib/single-file/dist/single-file-bootstrap.js
  21. 0 0
      lib/single-file/dist/single-file-frames.js
  22. 0 0
      lib/single-file/dist/single-file.js
  23. 54 61
      lib/single-file/index.js
  24. 263 263
      lib/single-file/modules/css-fonts-alt-minifier.js
  25. 287 278
      lib/single-file/modules/css-fonts-minifier.js
  26. 272 279
      lib/single-file/modules/css-matched-rules.js
  27. 54 52
      lib/single-file/modules/css-medias-alt-minifier.js
  28. 98 104
      lib/single-file/modules/css-rules-minifier.js
  29. 64 68
      lib/single-file/modules/html-images-alt-minifier.js
  30. 200 204
      lib/single-file/modules/html-minifier.js
  31. 136 140
      lib/single-file/modules/html-serializer.js
  32. 42 0
      lib/single-file/modules/index.js
  33. 329 313
      lib/single-file/processors/frame-tree/content/content-frame-tree.js
  34. 127 131
      lib/single-file/processors/hooks/content/content-hooks-frames.js
  35. 12 17
      lib/single-file/processors/hooks/content/content-hooks.js
  36. 34 0
      lib/single-file/processors/index.js
  37. 155 160
      lib/single-file/processors/lazy/content/content-lazy-loader.js
  38. 27 0
      lib/single-file/rollup.config.js
  39. 30 0
      lib/single-file/single-file-bootstrap.js
  40. 1924 1926
      lib/single-file/single-file-core.js
  41. 1 0
      lib/single-file/single-file-frames.js
  42. 415 416
      lib/single-file/single-file-helper.js
  43. 287 318
      lib/single-file/single-file-util.js
  44. 255 259
      lib/single-file/vendor/css-font-property-parser.js
  45. 365 369
      lib/single-file/vendor/css-media-query-parser.js
  46. 613 617
      lib/single-file/vendor/css-minifier.js
  47. 15 1
      lib/single-file/vendor/css-tree.js
  48. 20 24
      lib/single-file/vendor/css-unescape.js
  49. 262 266
      lib/single-file/vendor/html-srcset-parser.js
  50. 40 0
      lib/single-file/vendor/index.js
  51. 307 309
      lib/single-file/vendor/mime-type-parser.js
  52. 7 35
      manifest.json

+ 5 - 0
.eslintrc.js

@@ -1,3 +1,5 @@
+/* global module */
+
 module.exports = {
 	"env": {
 		"es6": true,
@@ -12,6 +14,9 @@ module.exports = {
 		"ecmaVersion": 2017,
 		"sourceType": "module"
 	},
+	"ignorePatterns": [
+		"dist/"
+	],
 	"rules": {
 		"indent": [
 			"error",

+ 3 - 3
build-extension.sh

@@ -10,13 +10,13 @@ jq "del(.options_page,.background.persistent,.optional_permissions[0],.optional_
 sed -i 's/207618107333-3pj2pmelhnl4sf3rpctghs9cean3q8nj/207618107333-7tjs1im1pighftpoepea2kvkubnfjj44/g' manifest.json
 sed -i 's/forceWebAuthFlow: false/forceWebAuthFlow: true/g' extension/core/bg/config.js
 sed -i 's/enabled: true/enabled: false/g' extension/core/bg/companion.js
-zip -r singlefile-extension-firefox.zip manifest.json common extension lib _locales
+zip -r singlefile-extension-firefox.zip manifest.json common extension lib/single-file/dist _locales
 mv config.copy.js extension/core/bg/config.js
 mv companion.copy.js extension/core/bg/companion.js
 
 jq "del(.browser_specific_settings,.permissions[0],.permissions[1],.permissions[2],.options_ui.browser_style)" manifest.copy.json > manifest.json
 sed -i 's/207618107333-3pj2pmelhnl4sf3rpctghs9cean3q8nj/207618107333-7tjs1im1pighftpoepea2kvkubnfjj44/g' manifest.json
-zip -r singlefile-extension-chromium.zip manifest.json common extension lib _locales
+zip -r singlefile-extension-chromium.zip manifest.json common extension lib/single-file/dist _locales
 
 cp extension/core/bg/config.js config.copy.js
 jq "del(.browser_specific_settings,.permissions[0],.permissions[1],.permissions[2],.options_ui.browser_style)" manifest.copy.json > manifest.json
@@ -26,7 +26,7 @@ mkdir _locales.copy
 cp -R _locales/* _locales.copy
 rm -rf _locales/*
 cp -R _locales.copy/en _locales
-zip -r singlefile-extension-edge.zip manifest.json common extension lib _locales
+zip -r singlefile-extension-edge.zip manifest.json common extension lib/single-file/dist _locales
 rm -rf _locales/*
 mv _locales.copy/* _locales
 rmdir _locales.copy

+ 2 - 26
cli/back-ends/common/scripts.js

@@ -26,35 +26,11 @@
 const fs = require("fs");
 
 const SCRIPTS = [
-	"lib/single-file/processors/hooks/content/content-hooks.js",
-	"lib/single-file/processors/hooks/content/content-hooks-web.js",
-	"lib/single-file/processors/hooks/content/content-hooks-frames.js",
-	"lib/single-file/processors/hooks/content/content-hooks-frames-web.js",
-	"lib/single-file/processors/frame-tree/content/content-frame-tree.js",
-	"lib/single-file/processors/lazy/content/content-lazy-loader.js",
-	"lib/single-file/single-file-util.js",
-	"lib/single-file/single-file-helper.js",
-	"lib/single-file/vendor/css-tree.js",
-	"lib/single-file/vendor/html-srcset-parser.js",
-	"lib/single-file/vendor/css-minifier.js",
-	"lib/single-file/vendor/css-font-property-parser.js",
-	"lib/single-file/vendor/css-unescape.js",
-	"lib/single-file/vendor/css-media-query-parser.js",
-	"lib/single-file/vendor/mime-type-parser.js",
-	"lib/single-file/modules/html-minifier.js",
-	"lib/single-file/modules/css-fonts-minifier.js",
-	"lib/single-file/modules/css-fonts-alt-minifier.js",
-	"lib/single-file/modules/css-matched-rules.js",
-	"lib/single-file/modules/css-medias-alt-minifier.js",
-	"lib/single-file/modules/css-rules-minifier.js",
-	"lib/single-file/modules/html-images-alt-minifier.js",
-	"lib/single-file/modules/html-serializer.js",
-	"lib/single-file/single-file-core.js",
 	"common/ui/content/content-infobar.js"
 ];
 
 const INDEX_SCRIPTS = [
-	"lib/single-file/index.js",
+	"lib/single-file/dist/single-file.js",	
 	"common/index.js"
 ];
 
@@ -69,7 +45,7 @@ exports.get = async options => {
 	let scripts = await readScriptFiles(INDEX_SCRIPTS, basePath);
 	const webScripts = {};
 	await Promise.all(WEB_SCRIPTS.map(async path => webScripts[path] = await readScriptFile(path, basePath)));
-	scripts += "this.singlefile.lib.getFileContent = filename => (" + JSON.stringify(webScripts) + ")[filename];\n";
+	scripts += "this.singlefile.getFileContent = filename => (" + JSON.stringify(webScripts) + ")[filename];\n";
 	scripts += await readScriptFiles(SCRIPTS, basePath);
 	scripts += await readScriptFiles(options && options.browserScripts ? options.browserScripts : [], "");
 	if (options.browserStylesheets && options.browserStylesheets.length) {

+ 1 - 1
cli/back-ends/jsdom.js

@@ -78,7 +78,7 @@ async function getPageData(win, options) {
 	if (options.browserWaitDelay) {
 		await new Promise(resolve => setTimeout(resolve, options.browserWaitDelay));
 	}
-	const pageData = await win.singlefile.lib.getPageData(options, { fetch: url => fetchResource(url, options) }, doc, win);
+	const pageData = await win.singlefile.getPageData(options, { fetch: url => fetchResource(url, options) }, doc, win);
 	if (options.includeInfobar) {
 		await win.common.ui.content.infobar.includeScript(pageData);
 	}

+ 1 - 1
cli/back-ends/playwright-chromium.js

@@ -101,7 +101,7 @@ async function getPageData(page, options) {
 		await page.waitForTimeout(options.browserWaitDelay);
 	}
 	return await page.evaluate(async options => {
-		const pageData = await singlefile.lib.getPageData(options);
+		const pageData = await singlefile.getPageData(options);
 		if (options.includeInfobar) {
 			await common.ui.content.infobar.includeScript(pageData);
 		}

+ 1 - 1
cli/back-ends/playwright-firefox.js

@@ -101,7 +101,7 @@ async function getPageData(page, options) {
 		await page.waitForTimeout(options.browserWaitDelay);
 	}
 	return await page.evaluate(async options => {
-		const pageData = await singlefile.lib.getPageData(options);
+		const pageData = await singlefile.getPageData(options);
 		if (options.includeInfobar) {
 			await common.ui.content.infobar.includeScript(pageData);
 		}

+ 1 - 1
cli/back-ends/puppeteer-firefox.js

@@ -106,7 +106,7 @@ async function getPageData(browser, page, options) {
 			await page.waitForTimeout(options.browserWaitDelay);
 		}
 		return await page.evaluate(async options => {
-			const pageData = await singlefile.lib.getPageData(options);
+			const pageData = await singlefile.getPageData(options);
 			if (options.includeInfobar) {
 				await common.ui.content.infobar.includeScript(pageData);
 			}

+ 1 - 1
cli/back-ends/puppeteer.js

@@ -130,7 +130,7 @@ async function getPageData(browser, page, options) {
 			await page.waitForTimeout(options.browserWaitDelay);
 		}
 		return await page.evaluate(async options => {
-			const pageData = await singlefile.lib.getPageData(options);
+			const pageData = await singlefile.getPageData(options);
 			if (options.includeInfobar) {
 				await common.ui.content.infobar.includeScript(pageData);
 			}

+ 1 - 1
cli/back-ends/webdriver-chromium.js

@@ -158,7 +158,7 @@ function getPageDataScript() {
 		.catch(error => callback({ error: error && error.toString() }));
 
 	async function getPageData() {
-		const pageData = await singlefile.lib.getPageData(options);
+		const pageData = await singlefile.getPageData(options);
 		if (options.includeInfobar) {
 			await common.ui.content.infobar.includeScript(pageData);
 		}

+ 1 - 1
cli/back-ends/webdriver-gecko.js

@@ -170,7 +170,7 @@ function getPageDataScript() {
 		.catch(error => callback({ error: error && error.toString() }));
 
 	async function getPageData() {
-		const pageData = await window.singlefile.lib.getPageData(options);
+		const pageData = await window.singlefile.getPageData(options);
 		if (options.includeInfobar) {
 			await window.common.ui.content.infobar.includeScript(pageData);
 		}

+ 2 - 2
common/ui/content/content-infobar.js

@@ -37,8 +37,8 @@ this.common.ui.content.infobar = this.common.ui.content.infobar || (() => {
 		let infobarContent;
 		if (browser && browser.runtime && browser.runtime.getURL) {
 			infobarContent = await (await fetch(browser.runtime.getURL(SCRIPT_PATH))).text();
-		} else if (singlefile.lib.getFileContent) {
-			infobarContent = singlefile.lib.getFileContent(SCRIPT_PATH);
+		} else if (singlefile.getFileContent) {
+			infobarContent = singlefile.getFileContent(SCRIPT_PATH);
 		}
 		let lastInfobarContent;
 		while (lastInfobarContent != infobarContent) {

+ 17 - 17
extension/core/content/content-bootstrap.js

@@ -119,7 +119,7 @@ this.extension.core.content.bootstrap = this.extension.core.content.bootstrap ||
 	}
 
 	async function autoSavePage() {
-		const helper = singlefile.lib.helper;
+		const helper = singlefile.helper;
 		if ((!autoSavingPage || autoSaveTimeout) && !pageAutoSaved) {
 			autoSavingPage = true;
 			if (options.autoSaveDelay && !autoSaveTimeout) {
@@ -129,21 +129,21 @@ this.extension.core.content.bootstrap = this.extension.core.content.bootstrap ||
 				let frames = [];
 				let framesSessionId;
 				autoSaveTimeout = null;
-				if (!options.removeFrames && singlefile.lib.processors.frameTree.content.frames && window.frames && window.frames.length) {
-					frames = await singlefile.lib.processors.frameTree.content.frames.getAsync(options);
+				if (!options.removeFrames && window.frames && window.frames.length) {
+					frames = await singlefile.processors.frameTree.getAsync(options);
 				}
 				framesSessionId = frames && frames.sessionId;
-				if (options.userScriptEnabled && helper.waitForUserScript) {
-					await helper.waitForUserScript(helper.ON_BEFORE_CAPTURE_EVENT_NAME);
+				if (options.userScriptEnabled && helper.waitForUserScript.callback) {
+					await helper.waitForUserScript.callback(helper.ON_BEFORE_CAPTURE_EVENT_NAME);
 				}
 				const docData = helper.preProcessDoc(document, window, options);
 				savePage(docData, frames);
 				if (framesSessionId) {
-					singlefile.lib.processors.frameTree.content.frames.cleanup(framesSessionId);
+					singlefile.processors.frameTree.cleanup(framesSessionId);
 				}
 				helper.postProcessDoc(document, docData.markedElements);
-				if (options.userScriptEnabled && helper.waitForUserScript) {
-					await helper.waitForUserScript(helper.ON_AFTER_CAPTURE_EVENT_NAME);
+				if (options.userScriptEnabled && helper.waitForUserScript.callback) {
+					await helper.waitForUserScript.callback(helper.ON_AFTER_CAPTURE_EVENT_NAME);
 				}
 				pageAutoSaved = true;
 				autoSavingPage = false;
@@ -164,14 +164,14 @@ this.extension.core.content.bootstrap = this.extension.core.content.bootstrap ||
 	}
 
 	function onUnload() {
-		const helper = singlefile.lib.helper;
+		const helper = singlefile.helper;
 		if (!pageAutoSaved || options.autoSaveUnload) {
 			let frames = [];
-			if (!options.removeFrames && singlefile.lib.processors.frameTree.content.frames && window.frames && window.frames.length) {
-				frames = singlefile.lib.processors.frameTree.content.frames.getSync(options);
+			if (!options.removeFrames && window.frames && window.frames.length) {
+				frames = singlefile.processors.frameTree.getSync(options);
 			}
-			if (options.userScriptEnabled && helper.waitForUserScript) {
-				helper.waitForUserScript(helper.ON_BEFORE_CAPTURE_EVENT_NAME);
+			if (options.userScriptEnabled && helper.waitForUserScript.callback) {
+				helper.waitForUserScript.callback(helper.ON_BEFORE_CAPTURE_EVENT_NAME);
 			}
 			const docData = helper.preProcessDoc(document, window, options);
 			savePage(docData, frames);
@@ -179,7 +179,7 @@ this.extension.core.content.bootstrap = this.extension.core.content.bootstrap ||
 	}
 
 	function savePage(docData, frames) {
-		const helper = singlefile.lib.helper;
+		const helper = singlefile.helper;
 		const updatedResources = extension.core.content.updatedResources;
 		const visitDate = extension.core.content.visitDate.getTime();
 		Object.keys(updatedResources).forEach(url => updatedResources[url].retrieved = false);
@@ -209,7 +209,7 @@ this.extension.core.content.bootstrap = this.extension.core.content.bootstrap ||
 			infobarElement.remove();
 		}
 		serializeShadowRoots(document);
-		const content = singlefile.lib.modules.serializer.process(document);
+		const content = singlefile.helper.serialize(document);
 		for (let blockIndex = 0; blockIndex * MAX_CONTENT_SIZE < content.length; blockIndex++) {
 			const message = {
 				method: "editor.open",
@@ -227,7 +227,7 @@ this.extension.core.content.bootstrap = this.extension.core.content.bootstrap ||
 	}
 
 	function detectSavedPage(document) {
-		const helper = singlefile.lib.helper;
+		const helper = singlefile.helper;
 		const firstDocumentChild = document.documentElement.firstChild;
 		return firstDocumentChild.nodeType == Node.COMMENT_NODE &&
 			(firstDocumentChild.textContent.includes(helper.COMMENT_HEADER) || firstDocumentChild.textContent.includes(helper.COMMENT_HEADER_LEGACY));
@@ -236,7 +236,7 @@ this.extension.core.content.bootstrap = this.extension.core.content.bootstrap ||
 	function serializeShadowRoots(node) {
 		const SHADOW_MODE_ATTRIBUTE_NAME = "shadowmode";
 		node.querySelectorAll("*").forEach(element => {
-			const shadowRoot = singlefile.lib.helper.getShadowRoot(element);
+			const shadowRoot = singlefile.helper.getShadowRoot(element);
 			if (shadowRoot) {
 				serializeShadowRoots(shadowRoot);
 				const templateElement = document.createElement("template");

+ 7 - 7
extension/core/content/content-main.js

@@ -32,7 +32,7 @@ this.extension.core.content.main = this.extension.core.content.main || (() => {
 
 	let ui, processor;
 
-	singlefile.lib.init({
+	singlefile.init({
 		fetch: extension.lib.fetch.content.resources.fetch,
 		frameFetch: extension.lib.fetch.content.resources.frameFetch
 	});
@@ -59,7 +59,7 @@ this.extension.core.content.main = this.extension.core.content.main || (() => {
 					browser.runtime.sendMessage({ method: "ui.processCancelled" });
 				}
 				if (message.options.loadDeferredImages) {
-					singlefile.lib.processors.lazy.content.loader.resetZoomLevel(message.options);
+					singlefile.processors.lazy.resetZoomLevel(message.options);
 				}
 				return {};
 			}
@@ -108,11 +108,11 @@ this.extension.core.content.main = this.extension.core.content.main || (() => {
 	}
 
 	async function processPage(options) {
-		const frames = singlefile.lib.processors.frameTree.content.frames;
+		const frames = singlefile.processors.frameTree;
 		let framesSessionId;
-		singlefile.lib.helper.initDoc(document);
+		singlefile.helper.initDoc(document);
 		ui.onStartPage(options);
-		processor = new singlefile.lib.SingleFile(options);
+		processor = new singlefile.SingleFile(options);
 		const preInitializationPromises = [];
 		options.insertSingleFileComment = true;
 		options.insertCanonicalLink = true;
@@ -133,7 +133,7 @@ this.extension.core.content.main = this.extension.core.content.main || (() => {
 				preInitializationPromises.push(frameTreePromise);
 			}
 			if (options.loadDeferredImages) {
-				const lazyLoadPromise = singlefile.lib.processors.lazy.content.loader.process(options);
+				const lazyLoadPromise = singlefile.processors.lazy.process(options);
 				ui.onLoadingDeferResources(options);
 				lazyLoadPromise.then(() => {
 					if (!processor.cancelled) {
@@ -149,7 +149,7 @@ this.extension.core.content.main = this.extension.core.content.main || (() => {
 				if (event.type == event.RESOURCES_INITIALIZED) {
 					maxIndex = event.detail.max;
 					if (options.loadDeferredImages) {
-						singlefile.lib.processors.lazy.content.loader.resetZoomLevel(options);
+						singlefile.processors.lazy.resetZoomLevel(options);
 					}
 				}
 				if (event.type == event.RESOURCES_INITIALIZED || event.type == event.RESOURCE_LOADED) {

+ 1 - 1
extension/index.js

@@ -23,5 +23,5 @@
 
 this.extension = this.extension || {
 	injectScript: (tabId, options) => this.extension.lib.core.bg.scripts.inject(tabId, options),
-	getPageData: (options, doc, win, initOptions = { fetch: this.extension.lib.fetch.content.resources.fetch }) => this.singlefile.lib.getPageData(options, initOptions, doc, win)
+	getPageData: (options, doc, win, initOptions = { fetch: this.extension.lib.fetch.content.resources.fetch }) => this.singlefile.getPageData(options, initOptions, doc, win)
 };

+ 2 - 26
extension/lib/single-file/core/bg/scripts.js

@@ -28,27 +28,7 @@ extension.lib.core.bg.scripts = (() => {
 	let contentScript, frameScript;
 
 	const contentScriptFiles = [
-		"lib/single-file/index.js",
-		"lib/single-file/vendor/css-font-property-parser.js",
-		"lib/single-file/vendor/css-unescape.js",
-		"lib/single-file/vendor/css-media-query-parser.js",
-		"lib/single-file/vendor/css-tree.js",
-		"lib/single-file/vendor/html-srcset-parser.js",
-		"lib/single-file/vendor/css-minifier.js",
-		"lib/single-file/vendor/mime-type-parser.js",
-		"lib/single-file/modules/html-minifier.js",
-		"lib/single-file/modules/html-serializer.js",
-		"lib/single-file/modules/html-images-alt-minifier.js",
-		"lib/single-file/modules/css-fonts-minifier.js",
-		"lib/single-file/modules/css-fonts-alt-minifier.js",
-		"lib/single-file/modules/css-matched-rules.js",
-		"lib/single-file/modules/css-rules-minifier.js",
-		"lib/single-file/modules/css-medias-alt-minifier.js",
-		"lib/single-file/single-file-util.js",
-		"lib/single-file/single-file-helper.js",
-		"lib/single-file/single-file-core.js",
-		"lib/single-file/processors/lazy/content/content-lazy-loader.js",
-		"lib/single-file/processors/hooks/content/content-hooks.js",
+		"lib/single-file/dist/single-file.js",
 		"extension/index.js",
 		"extension/lib/single-file/index.js",
 		"extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js",
@@ -56,11 +36,7 @@ extension.lib.core.bg.scripts = (() => {
 	];
 
 	const frameScriptFiles = [
-		"lib/single-file/index.js",
-		"lib/single-file/single-file-helper.js",
-		"lib/single-file/vendor/css-unescape.js",
-		"lib/single-file/processors/hooks/content/content-hooks-frames.js",
-		"lib/single-file/processors/frame-tree/content/content-frame-tree.js",
+		"lib/single-file/dist/single-file-frames.js",
 		"extension/index.js",
 		"extension/lib/single-file/index.js",
 		"extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js",

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

@@ -1726,7 +1726,7 @@ table {
 			doc.body.appendChild(element);
 			element.textContent = resource.content;
 		});
-		return singlefile.lib.modules.serializer.process(doc, compressHTML);
+		return singlefile.modules.serializer.process(doc, compressHTML);
 	}
 
 	function onUpdate(saved) {

+ 2 - 2
extension/ui/content/content-ui-main.js

@@ -25,7 +25,7 @@
 
 this.extension.ui.content.main = this.extension.ui.content.main || (() => {
 
-	const SELECTED_CONTENT_ATTRIBUTE_NAME = this.singlefile.lib.helper.SELECTED_CONTENT_ATTRIBUTE_NAME;
+	const SELECTED_CONTENT_ATTRIBUTE_NAME = this.singlefile.helper.SELECTED_CONTENT_ATTRIBUTE_NAME;
 
 	const MASK_TAGNAME = "singlefile-mask";
 	const MASK_CONTENT_CLASSNAME = "singlefile-mask-content";
@@ -37,7 +37,7 @@ this.extension.ui.content.main = this.extension.ui.content.main || (() => {
 	const LOGS_LINE_CLASSNAME = "singlefile-logs-line";
 	const LOGS_LINE_TEXT_ELEMENT_CLASSNAME = "singlefile-logs-line-text";
 	const LOGS_LINE_STATUS_ELEMENT_CLASSNAME = "singlefile-logs-line-icon";
-	const SINGLE_FILE_UI_ELEMENT_CLASS = this.singlefile.lib.helper.SINGLE_FILE_UI_ELEMENT_CLASS;
+	const SINGLE_FILE_UI_ELEMENT_CLASS = this.singlefile.helper.SINGLE_FILE_UI_ELEMENT_CLASS;
 	const SELECT_PX_THRESHOLD = 8;
 	const LOG_PANEL_DEFERRED_IMAGES_MESSAGE = browser.i18n.getMessage("logPanelDeferredImages");
 	const LOG_PANEL_FRAME_CONTENTS_MESSAGE = browser.i18n.getMessage("logPanelFrameContents");

+ 5 - 4
extension/ui/pages/editor.html

@@ -66,18 +66,19 @@
 		</div>
 	</div>
 	<iframe class="editor"
-		srcdoc="&lt;!DOCTYPE html&gt; &lt;body&gt;&lt;script src=/extension/ui/content/content-ui-editor-web.js&gt;&lt;/script&gt;&lt;script src=/lib/single-file/index.js&gt;&lt;/script&gt;&lt;script src=/lib/single-file/modules/html-serializer.js&gt;&lt;/script&gt;&lt;script src=/extension/lib/readability/Readability.js&gt;&lt;/script&gt;&lt;/script&gt;&lt;script src=/extension/lib/readability/Readability-readerable.js&gt;&lt;/script&gt;&lt;/body&gt;"
+		srcdoc="&lt;!DOCTYPE html&gt; &lt;body&gt;&lt;script src=/lib/single-file/dist/single-file.js&gt;&lt;/script&gt;&lt;script src=/extension/index.js&gt;&lt;/script&gt;&lt;script src=/extension/ui/content/content-ui-editor-web.js&gt;&lt;/script&gt;&lt;script src=/extension/lib/readability/Readability.js&gt;&lt;/script&gt;&lt;/script&gt;&lt;script src=/extension/lib/readability/Readability-readerable.js&gt;&lt;/script&gt;&lt;/body&gt;"
 		sandbox="allow-scripts allow-modals"></iframe>
 	<script type="text/javascript"
 		src="/extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js"></script>
-	<script src="/lib/single-file/index.js"></script>
+	<script src="/lib/single-file/dist/single-file.js"></script>
+	<script src="/common/index.js"></script>
+	<script src="/common/ui/content/content-infobar.js"></script>
+	<script src="/extension/index.js"></script>
 	<script src="/extension/lib/single-file/index.js"></script>
 	<script src="/extension/core/index.js"></script>
 	<script src="/extension/core/content/content-download.js"></script>
 	<script src="/extension/ui/index.js"></script>
 	<script src="/extension/ui/bg/ui-editor.js"></script>
-	<script src="/common/index.js"></script>
-	<script src="/common/ui/content/content-infobar.js"></script>
 </body>
 
 </html>

+ 3 - 0
lib/single-file/build-lib.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+rollup -c ./rollup.config.js

File diff suppressed because it is too large
+ 0 - 0
lib/single-file/dist/single-file-bootstrap.js


File diff suppressed because it is too large
+ 0 - 0
lib/single-file/dist/single-file-frames.js


File diff suppressed because it is too large
+ 0 - 0
lib/single-file/dist/single-file.js


+ 54 - 61
lib/single-file/index.js

@@ -21,74 +21,67 @@
  *   Source.
  */
 
-/* global window, globalThis */
+/* global globalThis */
 
-this.singlefile = this.singlefile || (globalThis => {
+import * as processors from "./processors/index.js";
+import * as vendor from "./vendor/index.js";
+import * as modules from "./modules/index.js";
+import * as core from "./single-file-core.js";
+import * as helper from "./single-file-helper.js";
+import * as util from "./single-file-util.js";
 
-	return {
-		lib: {
-			init,
-			getPageData,
-			processors: {
-				frameTree: {
-					content: {}
-				},
-				hooks: {
-					content: {}
-				},
-				lazy: {
-					content: {}
-				}
-			},
-			vendor: {},
-			modules: {},
-		}
-	};
+let SingleFile;
+export {
+	init,
+	getPageData,
+	processors,
+	vendor,
+	modules,
+	helper,
+	SingleFile
+};
 
-	function init(initOptions) {
-		const util = this.util.getInstance(initOptions);
-		this.SingleFile = this.core.getClass(util, this.vendor.cssTree);
-	}
+function init(initOptions) {
+	SingleFile = core.getClass(util.getInstance(initOptions), vendor.cssTree);
+}
 
-	async function getPageData(options = {}, initOptions, doc = globalThis.document, win = globalThis) {
-		const frames = this.processors.frameTree.content.frames;
-		let framesSessionId;
-		this.init(initOptions);
-		if (doc && win) {
-			this.helper.initDoc(doc);
-			const preInitializationPromises = [];
-			if (!options.saveRawPage) {
-				if (!options.removeFrames && frames && globalThis.frames && globalThis.frames.length) {
-					let frameTreePromise;
-					if (options.loadDeferredImages) {
-						frameTreePromise = new Promise(resolve => globalThis.setTimeout(() => resolve(frames.getAsync(options)), options.loadDeferredImagesMaxIdleTime - frames.TIMEOUT_INIT_REQUEST_MESSAGE));
-					} else {
-						frameTreePromise = frames.getAsync(options);
-					}
-					preInitializationPromises.push(frameTreePromise);
-				}
+async function getPageData(options = {}, initOptions, doc = globalThis.document, win = globalThis) {
+	const frames = processors.frameTree;
+	let framesSessionId;
+	init(initOptions);
+	if (doc && win) {
+		helper.initDoc(doc);
+		const preInitializationPromises = [];
+		if (!options.saveRawPage) {
+			if (!options.removeFrames && frames && globalThis.frames && globalThis.frames.length) {
+				let frameTreePromise;
 				if (options.loadDeferredImages) {
-					preInitializationPromises.push(this.processors.lazy.content.loader.process(options));
+					frameTreePromise = new Promise(resolve => globalThis.setTimeout(() => resolve(frames.getAsync(options)), options.loadDeferredImagesMaxIdleTime - frames.TIMEOUT_INIT_REQUEST_MESSAGE));
+				} else {
+					frameTreePromise = frames.getAsync(options);
 				}
+				preInitializationPromises.push(frameTreePromise);
 			}
-			[options.frames] = await Promise.all(preInitializationPromises);
-			framesSessionId = options.frames && options.frames.sessionId;
-		}
-		options.doc = doc;
-		options.win = win;
-		options.insertSingleFileComment = true;
-		options.insertCanonicalLink = true;
-		options.onprogress = event => {
-			if (event.type === event.RESOURCES_INITIALIZED && doc && win && options.loadDeferredImages) {
-				this.processors.lazy.content.loader.resetZoomLevel(options);
+			if (options.loadDeferredImages) {
+				preInitializationPromises.push(processors.lazy.process(options));
 			}
-		};
-		const processor = new this.SingleFile(options);
-		await processor.run();
-		if (framesSessionId) {
-			frames.cleanup(framesSessionId);
 		}
-		return await processor.getPageData();
+		[options.frames] = await Promise.all(preInitializationPromises);
+		framesSessionId = options.frames && options.frames.sessionId;
 	}
-
-})(typeof globalThis == "object" ? globalThis : window);
+	options.doc = doc;
+	options.win = win;
+	options.insertSingleFileComment = true;
+	options.insertCanonicalLink = true;
+	options.onprogress = event => {
+		if (event.type === event.RESOURCES_INITIALIZED && doc && win && options.loadDeferredImages) {
+			processors.lazy.resetZoomLevel(options);
+		}
+	};
+	const processor = new SingleFile(options);
+	await processor.run();
+	if (framesSessionId) {
+		frames.cleanup(framesSessionId);
+	}
+	return await processor.getPageData();
+}

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

@@ -21,311 +21,311 @@
  *   Source.
  */
 
-/* global window, globalThis */
+/* global globalThis */
 
-this.singlefile.lib.modules.fontsAltMinifier = this.singlefile.lib.modules.fontsAltMinifier || (globalThis => {
+import * as cssTree from "./../vendor/css-tree.js";
+import {
+	normalizeFontFamily,
+	getFontWeight
+} from "./../single-file-helper.js";
 
-	const singlefile = this.singlefile;
+const helper = {
+	normalizeFontFamily,
+	getFontWeight
+};
 
-	const FontFace = globalThis.FontFace;
+const FontFace = globalThis.FontFace;
 
-	const REGEXP_URL_SIMPLE_QUOTES_FN = /url\s*\(\s*'(.*?)'\s*\)/i;
-	const REGEXP_URL_DOUBLE_QUOTES_FN = /url\s*\(\s*"(.*?)"\s*\)/i;
-	const REGEXP_URL_NO_QUOTES_FN = /url\s*\(\s*(.*?)\s*\)/i;
-	const REGEXP_URL_FUNCTION = /(url|local)\(.*?\)\s*(,|$)/g;
-	const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
-	const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
-	const REGEXP_URL_FUNCTION_WOFF = /^url\(\s*["']?data:font\/(woff2?)/;
-	const REGEXP_URL_FUNCTION_WOFF_ALT = /^url\(\s*["']?data:application\/x-font-(woff)/;
-	const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
-	const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
-	const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
-	const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
-	const LOCAL_SOURCE = "local(";
-	const MEDIA_ALL = "all";
-	const FONT_STRETCHES = {
-		"ultra-condensed": "50%",
-		"extra-condensed": "62.5%",
-		"condensed": "75%",
-		"semi-condensed": "87.5%",
-		"normal": "100%",
-		"semi-expanded": "112.5%",
-		"expanded": "125%",
-		"extra-expanded": "150%",
-		"ultra-expanded": "200%"
-	};
-	const FONT_MAX_LOAD_DELAY = 5000;
+const REGEXP_URL_SIMPLE_QUOTES_FN = /url\s*\(\s*'(.*?)'\s*\)/i;
+const REGEXP_URL_DOUBLE_QUOTES_FN = /url\s*\(\s*"(.*?)"\s*\)/i;
+const REGEXP_URL_NO_QUOTES_FN = /url\s*\(\s*(.*?)\s*\)/i;
+const REGEXP_URL_FUNCTION = /(url|local)\(.*?\)\s*(,|$)/g;
+const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
+const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
+const REGEXP_URL_FUNCTION_WOFF = /^url\(\s*["']?data:font\/(woff2?)/;
+const REGEXP_URL_FUNCTION_WOFF_ALT = /^url\(\s*["']?data:application\/x-font-(woff)/;
+const REGEXP_FONT_FORMAT = /\.([^.?#]+)((\?|#).*?)?$/;
+const REGEXP_FONT_FORMAT_VALUE = /format\((.*?)\)\s*,?$/;
+const REGEXP_FONT_SRC = /(.*?)\s*,?$/;
+const EMPTY_URL_SOURCE = /^url\(["']?data:[^,]*,?["']?\)/;
+const LOCAL_SOURCE = "local(";
+const MEDIA_ALL = "all";
+const FONT_STRETCHES = {
+	"ultra-condensed": "50%",
+	"extra-condensed": "62.5%",
+	"condensed": "75%",
+	"semi-condensed": "87.5%",
+	"normal": "100%",
+	"semi-expanded": "112.5%",
+	"expanded": "125%",
+	"extra-expanded": "150%",
+	"ultra-expanded": "200%"
+};
+const FONT_MAX_LOAD_DELAY = 5000;
 
-	return {
-		process
-	};
+export {
+	process
+};
 
-	async function process(doc, stylesheets, fontURLs, fontTests) {
-		const fontsDetails = {
-			fonts: new Map(),
-			medias: new Map(),
-			supports: new Map()
-		};
-		const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
-		let sheetIndex = 0;
-		stylesheets.forEach(stylesheetInfo => {
-			const cssRules = stylesheetInfo.stylesheet.children;
-			if (cssRules) {
-				stats.rules.processed += cssRules.getSize();
-				stats.rules.discarded += cssRules.getSize();
-				if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != MEDIA_ALL) {
-					const mediaFontsDetails = createFontsDetailsInfo();
-					fontsDetails.medias.set("media-" + sheetIndex + "-" + stylesheetInfo.mediaText, mediaFontsDetails);
-					getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails);
-				} else {
-					getFontsDetails(doc, cssRules, sheetIndex, fontsDetails);
-				}
+async function process(doc, stylesheets, fontURLs, fontTests) {
+	const fontsDetails = {
+		fonts: new Map(),
+		medias: new Map(),
+		supports: new Map()
+	};
+	const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
+	let sheetIndex = 0;
+	stylesheets.forEach(stylesheetInfo => {
+		const cssRules = stylesheetInfo.stylesheet.children;
+		if (cssRules) {
+			stats.rules.processed += cssRules.getSize();
+			stats.rules.discarded += cssRules.getSize();
+			if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != MEDIA_ALL) {
+				const mediaFontsDetails = createFontsDetailsInfo();
+				fontsDetails.medias.set("media-" + sheetIndex + "-" + stylesheetInfo.mediaText, mediaFontsDetails);
+				getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails);
+			} else {
+				getFontsDetails(doc, cssRules, sheetIndex, fontsDetails);
 			}
-			sheetIndex++;
-		});
-		processFontDetails(fontsDetails);
-		await Promise.all([...stylesheets].map(async ([, stylesheetInfo], sheetIndex) => {
-			const cssRules = stylesheetInfo.stylesheet.children;
-			const media = stylesheetInfo.mediaText;
-			if (cssRules) {
-				if (media && media != MEDIA_ALL) {
-					await processFontFaceRules(cssRules, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + media), fontURLs, fontTests, stats);
-				} else {
-					await processFontFaceRules(cssRules, sheetIndex, fontsDetails, fontURLs, fontTests, stats);
-				}
-				stats.rules.discarded -= cssRules.getSize();
+		}
+		sheetIndex++;
+	});
+	processFontDetails(fontsDetails);
+	await Promise.all([...stylesheets].map(async ([, stylesheetInfo], sheetIndex) => {
+		const cssRules = stylesheetInfo.stylesheet.children;
+		const media = stylesheetInfo.mediaText;
+		if (cssRules) {
+			if (media && media != MEDIA_ALL) {
+				await processFontFaceRules(cssRules, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + media), fontURLs, fontTests, stats);
+			} else {
+				await processFontFaceRules(cssRules, sheetIndex, fontsDetails, fontURLs, fontTests, stats);
 			}
-		}));
-		return stats;
-	}
+			stats.rules.discarded -= cssRules.getSize();
+		}
+	}));
+	return stats;
+}
 
-	function getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		let mediaIndex = 0, supportsIndex = 0;
-		cssRules.forEach(ruleData => {
-			if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
-				const mediaText = cssTree.generate(ruleData.prelude);
-				const fontsDetails = createFontsDetailsInfo();
-				mediaFontsDetails.medias.set("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText, fontsDetails);
-				mediaIndex++;
-				getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
-			} else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
-				const supportsText = cssTree.generate(ruleData.prelude);
-				const fontsDetails = createFontsDetailsInfo();
-				mediaFontsDetails.supports.set("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText, fontsDetails);
-				supportsIndex++;
-				getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
-			} else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && ruleData.block && ruleData.block.children) {
-				const fontKey = getFontKey(ruleData);
-				let fontInfo = mediaFontsDetails.fonts.get(fontKey);
-				if (!fontInfo) {
-					fontInfo = [];
-					mediaFontsDetails.fonts.set(fontKey, fontInfo);
-				}
-				const src = getPropertyValue(ruleData, "src");
-				if (src) {
-					const fontSources = src.match(REGEXP_URL_FUNCTION);
-					if (fontSources) {
-						fontSources.forEach(source => fontInfo.unshift(source));
-					}
+function getFontsDetails(doc, cssRules, sheetIndex, mediaFontsDetails) {
+	let mediaIndex = 0, supportsIndex = 0;
+	cssRules.forEach(ruleData => {
+		if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
+			const mediaText = cssTree.generate(ruleData.prelude);
+			const fontsDetails = createFontsDetailsInfo();
+			mediaFontsDetails.medias.set("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText, fontsDetails);
+			mediaIndex++;
+			getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
+		} else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
+			const supportsText = cssTree.generate(ruleData.prelude);
+			const fontsDetails = createFontsDetailsInfo();
+			mediaFontsDetails.supports.set("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText, fontsDetails);
+			supportsIndex++;
+			getFontsDetails(doc, ruleData.block.children, sheetIndex, fontsDetails);
+		} else if (ruleData.type == "Atrule" && ruleData.name == "font-face" && ruleData.block && ruleData.block.children) {
+			const fontKey = getFontKey(ruleData);
+			let fontInfo = mediaFontsDetails.fonts.get(fontKey);
+			if (!fontInfo) {
+				fontInfo = [];
+				mediaFontsDetails.fonts.set(fontKey, fontInfo);
+			}
+			const src = getPropertyValue(ruleData, "src");
+			if (src) {
+				const fontSources = src.match(REGEXP_URL_FUNCTION);
+				if (fontSources) {
+					fontSources.forEach(source => fontInfo.unshift(source));
 				}
 			}
-		});
-	}
+		}
+	});
+}
 
-	function processFontDetails(fontsDetails) {
-		fontsDetails.fonts.forEach((fontInfo, fontKey) => {
-			fontsDetails.fonts.set(fontKey, fontInfo.map(fontSource => {
-				const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
-				let fontFormat;
-				const urlMatch = fontSource.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
-					fontSource.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
-					fontSource.match(REGEXP_URL_NO_QUOTES_FN);
-				const fontUrl = urlMatch && urlMatch[1];
+function processFontDetails(fontsDetails) {
+	fontsDetails.fonts.forEach((fontInfo, fontKey) => {
+		fontsDetails.fonts.set(fontKey, fontInfo.map(fontSource => {
+			const fontFormatMatch = fontSource.match(REGEXP_FONT_FORMAT_VALUE);
+			let fontFormat;
+			const urlMatch = fontSource.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
+				fontSource.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
+				fontSource.match(REGEXP_URL_NO_QUOTES_FN);
+			const fontUrl = urlMatch && urlMatch[1];
+			if (fontFormatMatch && fontFormatMatch[1]) {
+				fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
+			}
+			if (!fontFormat) {
+				const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
 				if (fontFormatMatch && fontFormatMatch[1]) {
-					fontFormat = fontFormatMatch[1].replace(REGEXP_SIMPLE_QUOTES_STRING, "$1").replace(REGEXP_DOUBLE_QUOTES_STRING, "$1").toLowerCase();
-				}
-				if (!fontFormat) {
-					const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF);
+					fontFormat = fontFormatMatch[1];
+				} else {
+					const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
 					if (fontFormatMatch && fontFormatMatch[1]) {
 						fontFormat = fontFormatMatch[1];
-					} else {
-						const fontFormatMatch = fontSource.match(REGEXP_URL_FUNCTION_WOFF_ALT);
-						if (fontFormatMatch && fontFormatMatch[1]) {
-							fontFormat = fontFormatMatch[1];
-						}
 					}
 				}
-				if (!fontFormat && fontUrl) {
-					const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
-					if (fontFormatMatch && fontFormatMatch[1]) {
-						fontFormat = fontFormatMatch[1];
-					}
+			}
+			if (!fontFormat && fontUrl) {
+				const fontFormatMatch = fontUrl.match(REGEXP_FONT_FORMAT);
+				if (fontFormatMatch && fontFormatMatch[1]) {
+					fontFormat = fontFormatMatch[1];
 				}
-				return { src: fontSource.match(REGEXP_FONT_SRC)[1], fontUrl, format: fontFormat };
-			}));
-		});
-		fontsDetails.medias.forEach(mediaFontsDetails => processFontDetails(mediaFontsDetails));
-		fontsDetails.supports.forEach(supportsFontsDetails => processFontDetails(supportsFontsDetails));
-	}
+			}
+			return { src: fontSource.match(REGEXP_FONT_SRC)[1], fontUrl, format: fontFormat };
+		}));
+	});
+	fontsDetails.medias.forEach(mediaFontsDetails => processFontDetails(mediaFontsDetails));
+	fontsDetails.supports.forEach(supportsFontsDetails => processFontDetails(supportsFontsDetails));
+}
 
-	async function processFontFaceRules(cssRules, sheetIndex, fontsDetails, fontURLs, fontTests, stats) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		const removedRules = [];
-		let mediaIndex = 0, supportsIndex = 0;
-		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
-			const ruleData = cssRule.data;
-			if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
-				const mediaText = cssTree.generate(ruleData.prelude);
-				await processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText), fontURLs, fontTests, stats);
-				mediaIndex++;
-			} else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
-				const supportsText = cssTree.generate(ruleData.prelude);
-				await processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.supports.get("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText), fontURLs, fontTests, stats);
-				supportsIndex++;
-			} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
-				const key = getFontKey(ruleData);
-				const fontInfo = fontsDetails.fonts.get(key);
-				if (fontInfo) {
-					const processed = await processFontFaceRule(ruleData, fontInfo, fontURLs, fontTests, stats);
-					if (processed) {
-						fontsDetails.fonts.delete(key);
-					}
-				} else {
-					removedRules.push(cssRule);
+async function processFontFaceRules(cssRules, sheetIndex, fontsDetails, fontURLs, fontTests, stats) {
+	const removedRules = [];
+	let mediaIndex = 0, supportsIndex = 0;
+	for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
+		const ruleData = cssRule.data;
+		if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude) {
+			const mediaText = cssTree.generate(ruleData.prelude);
+			await processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.medias.get("media-" + sheetIndex + "-" + mediaIndex + "-" + mediaText), fontURLs, fontTests, stats);
+			mediaIndex++;
+		} else if (ruleData.type == "Atrule" && ruleData.name == "supports" && ruleData.block && ruleData.block.children && ruleData.prelude) {
+			const supportsText = cssTree.generate(ruleData.prelude);
+			await processFontFaceRules(ruleData.block.children, sheetIndex, fontsDetails.supports.get("supports-" + sheetIndex + "-" + supportsIndex + "-" + supportsText), fontURLs, fontTests, stats);
+			supportsIndex++;
+		} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
+			const key = getFontKey(ruleData);
+			const fontInfo = fontsDetails.fonts.get(key);
+			if (fontInfo) {
+				const processed = await processFontFaceRule(ruleData, fontInfo, fontURLs, fontTests, stats);
+				if (processed) {
+					fontsDetails.fonts.delete(key);
 				}
+			} else {
+				removedRules.push(cssRule);
 			}
 		}
-		removedRules.forEach(cssRule => cssRules.remove(cssRule));
 	}
+	removedRules.forEach(cssRule => cssRules.remove(cssRule));
+}
 
-	async function processFontFaceRule(ruleData, fontInfo, fontURLs, fontTests, stats) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-
-		const removedNodes = [];
-		for (let node = ruleData.block.children.head; node; node = node.next) {
-			if (node.data.property == "src") {
-				removedNodes.push(node);
-			}
+async function processFontFaceRule(ruleData, fontInfo, fontURLs, fontTests, stats) {
+	const removedNodes = [];
+	for (let node = ruleData.block.children.head; node; node = node.next) {
+		if (node.data.property == "src") {
+			removedNodes.push(node);
 		}
-		removedNodes.pop();
-		removedNodes.forEach(node => ruleData.block.children.remove(node));
-		const srcDeclaration = ruleData.block.children.filter(node => node.property == "src").tail;
-		if (srcDeclaration) {
-			await Promise.all(fontInfo.map(async (source, sourceIndex) => {
-				if (fontTests.has(source.src)) {
-					source.valid = fontTests.get(source.src);
-				} else {
-					if (FontFace) {
-						const fontFace = new FontFace("test-font", source.src);
-						try {
-							await fontFace.load();
-							await fontFace.loaded;
-							source.valid = true;
-						} catch (error) {
-							const declarationFontURLs = fontURLs.get(srcDeclaration.data);
-							if (declarationFontURLs) {
-								const fontURL = declarationFontURLs[declarationFontURLs.length - sourceIndex - 1];
-								if (fontURL) {
-									const fontFace = new FontFace("test-font", "url(" + fontURL + ")");
-									try {
-										await Promise.race(
-											[
-												fontFace.load().then(() => fontFace.loaded).then(() => source.valid = true),
-												new Promise(resolve => globalThis.setTimeout(() => { source.valid = true; resolve(); }, FONT_MAX_LOAD_DELAY))
-											]);
-									} catch (error) {
-										// ignored
-									}
+	}
+	removedNodes.pop();
+	removedNodes.forEach(node => ruleData.block.children.remove(node));
+	const srcDeclaration = ruleData.block.children.filter(node => node.property == "src").tail;
+	if (srcDeclaration) {
+		await Promise.all(fontInfo.map(async (source, sourceIndex) => {
+			if (fontTests.has(source.src)) {
+				source.valid = fontTests.get(source.src);
+			} else {
+				if (FontFace) {
+					const fontFace = new FontFace("test-font", source.src);
+					try {
+						await fontFace.load();
+						await fontFace.loaded;
+						source.valid = true;
+					} catch (error) {
+						const declarationFontURLs = fontURLs.get(srcDeclaration.data);
+						if (declarationFontURLs) {
+							const fontURL = declarationFontURLs[declarationFontURLs.length - sourceIndex - 1];
+							if (fontURL) {
+								const fontFace = new FontFace("test-font", "url(" + fontURL + ")");
+								try {
+									await Promise.race(
+										[
+											fontFace.load().then(() => fontFace.loaded).then(() => source.valid = true),
+											new Promise(resolve => globalThis.setTimeout(() => { source.valid = true; resolve(); }, FONT_MAX_LOAD_DELAY))
+										]);
+								} catch (error) {
+									// ignored
 								}
-							} else {
-								source.valid = true;
 							}
+						} else {
+							source.valid = true;
 						}
-					} else {
-						source.valid = true;
 					}
-					fontTests.set(source.src, source.valid);
+				} else {
+					source.valid = true;
 				}
-			}));
-			const findSource = (fontFormat, testValidity) => fontInfo.find(source => !source.src.match(EMPTY_URL_SOURCE) && source.format == fontFormat && (!testValidity || source.valid));
-			const filterSource = fontSource => fontInfo.filter(source => source == fontSource || source.src.startsWith(LOCAL_SOURCE));
-			stats.fonts.processed += fontInfo.length;
-			stats.fonts.discarded += fontInfo.length;
-			const woffFontFound = findSource("woff2-variations", true) || findSource("woff2", true) || findSource("woff", true);
-			if (woffFontFound) {
-				fontInfo = filterSource(woffFontFound);
+				fontTests.set(source.src, source.valid);
+			}
+		}));
+		const findSource = (fontFormat, testValidity) => fontInfo.find(source => !source.src.match(EMPTY_URL_SOURCE) && source.format == fontFormat && (!testValidity || source.valid));
+		const filterSource = fontSource => fontInfo.filter(source => source == fontSource || source.src.startsWith(LOCAL_SOURCE));
+		stats.fonts.processed += fontInfo.length;
+		stats.fonts.discarded += fontInfo.length;
+		const woffFontFound = findSource("woff2-variations", true) || findSource("woff2", true) || findSource("woff", true);
+		if (woffFontFound) {
+			fontInfo = filterSource(woffFontFound);
+		} else {
+			const ttfFontFound = findSource("truetype-variations", true) || findSource("truetype", true);
+			if (ttfFontFound) {
+				fontInfo = filterSource(ttfFontFound);
 			} else {
-				const ttfFontFound = findSource("truetype-variations", true) || findSource("truetype", true);
-				if (ttfFontFound) {
-					fontInfo = filterSource(ttfFontFound);
+				const otfFontFound = findSource("opentype") || findSource("embedded-opentype");
+				if (otfFontFound) {
+					fontInfo = filterSource(otfFontFound);
 				} else {
-					const otfFontFound = findSource("opentype") || findSource("embedded-opentype");
-					if (otfFontFound) {
-						fontInfo = filterSource(otfFontFound);
-					} else {
-						fontInfo = fontInfo.filter(source => !source.src.match(EMPTY_URL_SOURCE) && (source.valid) || source.src.startsWith(LOCAL_SOURCE));
-					}
+					fontInfo = fontInfo.filter(source => !source.src.match(EMPTY_URL_SOURCE) && (source.valid) || source.src.startsWith(LOCAL_SOURCE));
 				}
 			}
-			stats.fonts.discarded -= fontInfo.length;
-			fontInfo.reverse();
-			try {
-				srcDeclaration.data.value = cssTree.parse(fontInfo.map(fontSource => fontSource.src).join(","), { context: "value" });
-			}
-			catch (error) {
-				// ignored
-			}
-			return true;
-		} else {
-			return false;
 		}
+		stats.fonts.discarded -= fontInfo.length;
+		fontInfo.reverse();
+		try {
+			srcDeclaration.data.value = cssTree.parse(fontInfo.map(fontSource => fontSource.src).join(","), { context: "value" });
+		}
+		catch (error) {
+			// ignored
+		}
+		return true;
+	} else {
+		return false;
 	}
+}
 
-	function getPropertyValue(ruleData, propertyName) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		let property;
-		if (ruleData.block.children) {
-			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) {
+function getPropertyValue(ruleData, propertyName) {
+	let property;
+	if (ruleData.block.children) {
+		property = ruleData.block.children.filter(node => {
 			try {
-				return cssTree.generate(property.data.value);
+				return node.property == propertyName && !cssTree.generate(node.value).match(/\\9$/);
 			} catch (error) {
-				// ignored
+				return node.property == propertyName;
 			}
+		}).tail;
+	}
+	if (property) {
+		try {
+			return cssTree.generate(property.data.value);
+		} catch (error) {
+			// ignored
 		}
 	}
+}
 
-	function getFontKey(ruleData) {
-		return JSON.stringify([
-			singlefile.lib.helper.normalizeFontFamily(getPropertyValue(ruleData, "font-family")),
-			singlefile.lib.helper.getFontWeight(getPropertyValue(ruleData, "font-weight") || "400"),
-			getPropertyValue(ruleData, "font-style") || "normal",
-			getPropertyValue(ruleData, "unicode-range"),
-			getFontStretch(getPropertyValue(ruleData, "font-stretch")),
-			getPropertyValue(ruleData, "font-variant") || "normal",
-			getPropertyValue(ruleData, "font-feature-settings"),
-			getPropertyValue(ruleData, "font-variation-settings")
-		]);
-	}
+function getFontKey(ruleData) {
+	return JSON.stringify([
+		helper.normalizeFontFamily(getPropertyValue(ruleData, "font-family")),
+		helper.getFontWeight(getPropertyValue(ruleData, "font-weight") || "400"),
+		getPropertyValue(ruleData, "font-style") || "normal",
+		getPropertyValue(ruleData, "unicode-range"),
+		getFontStretch(getPropertyValue(ruleData, "font-stretch")),
+		getPropertyValue(ruleData, "font-variant") || "normal",
+		getPropertyValue(ruleData, "font-feature-settings"),
+		getPropertyValue(ruleData, "font-variation-settings")
+	]);
+}
 
-	function getFontStretch(stretch) {
-		return FONT_STRETCHES[stretch] || stretch;
-	}
+function getFontStretch(stretch) {
+	return FONT_STRETCHES[stretch] || stretch;
+}
 
-	function createFontsDetailsInfo() {
-		return {
-			fonts: new Map(),
-			medias: new Map(),
-			supports: new Map()
-		};
-	}
-
-})(typeof globalThis == "object" ? globalThis : window);
+function createFontsDetailsInfo() {
+	return {
+		fonts: new Map(),
+		medias: new Map(),
+		supports: new Map()
+	};
+}

+ 287 - 278
lib/single-file/modules/css-fonts-minifier.js

@@ -21,337 +21,346 @@
  *   Source.
  */
 
-/* global window, globalThis */
+/* global globalThis */
 
-this.singlefile.lib.modules.fontsMinifier = this.singlefile.lib.modules.fontsMinifier || (globalThis => {
+import * as cssTree from "./../vendor/css-tree.js";
+import * as fontPropertyParser from "./../vendor/css-font-property-parser.js";
+import {
+	normalizeFontFamily,
+	flatten,
+	getFontWeight,
+	removeQuotes
+} from "./../single-file-helper.js";
 
-	const singlefile = this.singlefile;
+const helper = {
+	normalizeFontFamily,
+	flatten,
+	getFontWeight,
+	removeQuotes
+};
 
-	const REGEXP_COMMA = /\s*,\s*/;
-	const REGEXP_DASH = /-/;
-	const REGEXP_QUESTION_MARK = /\?/g;
-	const REGEXP_STARTS_U_PLUS = /^U\+/i;
-	const VALID_FONT_STYLES = [/^normal$/, /^italic$/, /^oblique$/, /^oblique\s+/];
+const REGEXP_COMMA = /\s*,\s*/;
+const REGEXP_DASH = /-/;
+const REGEXP_QUESTION_MARK = /\?/g;
+const REGEXP_STARTS_U_PLUS = /^U\+/i;
+const VALID_FONT_STYLES = [/^normal$/, /^italic$/, /^oblique$/, /^oblique\s+/];
 
-	return {
-		process
-	};
+export {
+	process
+};
 
-	function process(doc, stylesheets, styles, options) {
-		const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
-		const fontsInfo = { declared: [], used: [] };
-		const workStyleElement = doc.createElement("style");
-		let docContent = "";
-		doc.body.appendChild(workStyleElement);
-		stylesheets.forEach(stylesheetInfo => {
-			const cssRules = stylesheetInfo.stylesheet.children;
-			if (cssRules) {
-				stats.processed += cssRules.getSize();
-				stats.discarded += cssRules.getSize();
-				getFontsInfo(cssRules, fontsInfo);
-				docContent = getRulesTextContent(doc, cssRules, workStyleElement, docContent);
-			}
-		});
-		styles.forEach(declarations => {
-			const fontFamilyNames = getFontFamilyNames(declarations);
-			if (fontFamilyNames.length) {
-				fontsInfo.used.push(fontFamilyNames);
-			}
-			docContent = getDeclarationsTextContent(declarations.children, workStyleElement, docContent);
-		});
-		workStyleElement.remove();
-		docContent += doc.body.innerText;
-		if (globalThis.getComputedStyle && options.doc) {
-			fontsInfo.used = fontsInfo.used.map(fontNames => fontNames.map(familyName => {
-				const matchedVar = familyName.match(/^var\((--.*)\)$/);
-				if (matchedVar && matchedVar[1]) {
-					const computedFamilyName = globalThis.getComputedStyle(options.doc.body).getPropertyValue(matchedVar[1]);
-					return (computedFamilyName && computedFamilyName.split(",").map(name => singlefile.lib.helper.normalizeFontFamily(name))) || familyName;
-				}
-				return familyName;
-			}));
-			fontsInfo.used = fontsInfo.used.map(fontNames => singlefile.lib.helper.flatten(fontNames));
+function process(doc, stylesheets, styles, options) {
+	const stats = { rules: { processed: 0, discarded: 0 }, fonts: { processed: 0, discarded: 0 } };
+	const fontsInfo = { declared: [], used: [] };
+	const workStyleElement = doc.createElement("style");
+	let docContent = "";
+	doc.body.appendChild(workStyleElement);
+	stylesheets.forEach(stylesheetInfo => {
+		const cssRules = stylesheetInfo.stylesheet.children;
+		if (cssRules) {
+			stats.processed += cssRules.getSize();
+			stats.discarded += cssRules.getSize();
+			getFontsInfo(cssRules, fontsInfo);
+			docContent = getRulesTextContent(doc, cssRules, workStyleElement, docContent);
 		}
-		const variableFound = fontsInfo.used.find(fontNames => fontNames.find(fontName => fontName.startsWith("var(--")));
-		let unusedFonts, filteredUsedFonts;
-		if (variableFound) {
-			unusedFonts = [];
-		} else {
-			filteredUsedFonts = new Map();
-			fontsInfo.used.forEach(fontNames => fontNames.forEach(familyName => {
-				if (fontsInfo.declared.find(fontInfo => fontInfo.fontFamily == familyName)) {
-					const optionalData = options.usedFonts && options.usedFonts.filter(fontInfo => fontInfo[0] == familyName);
-					if (optionalData && optionalData.length) {
-						filteredUsedFonts.set(familyName, optionalData);
-					}
-				}
-			}));
-			unusedFonts = fontsInfo.declared.filter(fontInfo => !filteredUsedFonts.has(fontInfo.fontFamily));
+	});
+	styles.forEach(declarations => {
+		const fontFamilyNames = getFontFamilyNames(declarations);
+		if (fontFamilyNames.length) {
+			fontsInfo.used.push(fontFamilyNames);
 		}
-		stylesheets.forEach(stylesheetInfo => {
-			const cssRules = stylesheetInfo.stylesheet.children;
-			if (cssRules) {
-				filterUnusedFonts(cssRules, fontsInfo.declared, unusedFonts, filteredUsedFonts, docContent);
-				stats.rules.discarded -= cssRules.getSize();
+		docContent = getDeclarationsTextContent(declarations.children, workStyleElement, docContent);
+	});
+	workStyleElement.remove();
+	docContent += doc.body.innerText;
+	if (globalThis.getComputedStyle && options.doc) {
+		fontsInfo.used = fontsInfo.used.map(fontNames => fontNames.map(familyName => {
+			const matchedVar = familyName.match(/^var\((--.*)\)$/);
+			if (matchedVar && matchedVar[1]) {
+				const computedFamilyName = globalThis.getComputedStyle(options.doc.body).getPropertyValue(matchedVar[1]);
+				return (computedFamilyName && computedFamilyName.split(",").map(name => helper.normalizeFontFamily(name))) || familyName;
 			}
-		});
-		return stats;
+			return familyName;
+		}));
+		fontsInfo.used = fontsInfo.used.map(fontNames => helper.flatten(fontNames));
 	}
-
-	function getFontsInfo(cssRules, fontsInfo) {
-		cssRules.forEach(ruleData => {
-			if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports") && ruleData.block && ruleData.block.children) {
-				getFontsInfo(ruleData.block.children, fontsInfo);
-			} else if (ruleData.type == "Rule") {
-				const fontFamilyNames = getFontFamilyNames(ruleData.block);
-				if (fontFamilyNames.length) {
-					fontsInfo.used.push(fontFamilyNames);
-				}
-			} else {
-				if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
-					const fontFamily = singlefile.lib.helper.normalizeFontFamily(getDeclarationValue(ruleData.block.children, "font-family"));
-					if (fontFamily) {
-						const fontWeight = getDeclarationValue(ruleData.block.children, "font-weight") || "400";
-						const fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
-						const fontVariant = getDeclarationValue(ruleData.block.children, "font-variant") || "normal";
-						fontWeight.split(",").forEach(weightValue =>
-							fontsInfo.declared.push({ fontFamily, fontWeight: singlefile.lib.helper.getFontWeight(singlefile.lib.helper.removeQuotes(weightValue)), fontStyle, fontVariant }));
-					}
+	const variableFound = fontsInfo.used.find(fontNames => fontNames.find(fontName => fontName.startsWith("var(--")));
+	let unusedFonts, filteredUsedFonts;
+	if (variableFound) {
+		unusedFonts = [];
+	} else {
+		filteredUsedFonts = new Map();
+		fontsInfo.used.forEach(fontNames => fontNames.forEach(familyName => {
+			if (fontsInfo.declared.find(fontInfo => fontInfo.fontFamily == familyName)) {
+				const optionalData = options.usedFonts && options.usedFonts.filter(fontInfo => fontInfo[0] == familyName);
+				if (optionalData && optionalData.length) {
+					filteredUsedFonts.set(familyName, optionalData);
 				}
 			}
-		});
+		}));
+		unusedFonts = fontsInfo.declared.filter(fontInfo => !filteredUsedFonts.has(fontInfo.fontFamily));
 	}
+	stylesheets.forEach(stylesheetInfo => {
+		const cssRules = stylesheetInfo.stylesheet.children;
+		if (cssRules) {
+			filterUnusedFonts(cssRules, fontsInfo.declared, unusedFonts, filteredUsedFonts, docContent);
+			stats.rules.discarded -= cssRules.getSize();
+		}
+	});
+	return stats;
+}
 
-	function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, filteredUsedFonts, docContent) {
-		const removedRules = [];
-		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
-			const ruleData = cssRule.data;
-			if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports") && ruleData.block && ruleData.block.children) {
-				filterUnusedFonts(ruleData.block.children, declaredFonts, unusedFonts, filteredUsedFonts, docContent);
-			} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
-				const fontFamily = singlefile.lib.helper.normalizeFontFamily(getDeclarationValue(ruleData.block.children, "font-family"));
+function getFontsInfo(cssRules, fontsInfo) {
+	cssRules.forEach(ruleData => {
+		if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports") && ruleData.block && ruleData.block.children) {
+			getFontsInfo(ruleData.block.children, fontsInfo);
+		} else if (ruleData.type == "Rule") {
+			const fontFamilyNames = getFontFamilyNames(ruleData.block);
+			if (fontFamilyNames.length) {
+				fontsInfo.used.push(fontFamilyNames);
+			}
+		} else {
+			if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
+				const fontFamily = helper.normalizeFontFamily(getDeclarationValue(ruleData.block.children, "font-family"));
 				if (fontFamily) {
-					const unicodeRange = getDeclarationValue(ruleData.block.children, "unicode-range");
-					if (unusedFonts.find(fontInfo => fontInfo.fontFamily == fontFamily) || !testUnicodeRange(docContent, unicodeRange) || !testUsedFont(ruleData, fontFamily, declaredFonts, filteredUsedFonts)) {
-						removedRules.push(cssRule);
-					}
+					const fontWeight = getDeclarationValue(ruleData.block.children, "font-weight") || "400";
+					const fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
+					const fontVariant = getDeclarationValue(ruleData.block.children, "font-variant") || "normal";
+					fontWeight.split(",").forEach(weightValue =>
+						fontsInfo.declared.push({ fontFamily, fontWeight: helper.getFontWeight(helper.removeQuotes(weightValue)), fontStyle, fontVariant }));
 				}
-				const removedDeclarations = [];
-				for (let declaration = ruleData.block.children.head; declaration; declaration = declaration.next) {
-					if (declaration.data.property == "font-display") {
-						removedDeclarations.push(declaration);
-					}
+			}
+		}
+	});
+}
+
+function filterUnusedFonts(cssRules, declaredFonts, unusedFonts, filteredUsedFonts, docContent) {
+	const removedRules = [];
+	for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
+		const ruleData = cssRule.data;
+		if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports") && ruleData.block && ruleData.block.children) {
+			filterUnusedFonts(ruleData.block.children, declaredFonts, unusedFonts, filteredUsedFonts, docContent);
+		} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
+			const fontFamily = helper.normalizeFontFamily(getDeclarationValue(ruleData.block.children, "font-family"));
+			if (fontFamily) {
+				const unicodeRange = getDeclarationValue(ruleData.block.children, "unicode-range");
+				if (unusedFonts.find(fontInfo => fontInfo.fontFamily == fontFamily) || !testUnicodeRange(docContent, unicodeRange) || !testUsedFont(ruleData, fontFamily, declaredFonts, filteredUsedFonts)) {
+					removedRules.push(cssRule);
 				}
-				if (removedDeclarations.length) {
-					removedDeclarations.forEach(removedDeclaration => ruleData.block.children.remove(removedDeclaration));
+			}
+			const removedDeclarations = [];
+			for (let declaration = ruleData.block.children.head; declaration; declaration = declaration.next) {
+				if (declaration.data.property == "font-display") {
+					removedDeclarations.push(declaration);
 				}
 			}
+			if (removedDeclarations.length) {
+				removedDeclarations.forEach(removedDeclaration => ruleData.block.children.remove(removedDeclaration));
+			}
 		}
-		removedRules.forEach(cssRule => cssRules.remove(cssRule));
 	}
+	removedRules.forEach(cssRule => cssRules.remove(cssRule));
+}
 
-	function testUsedFont(ruleData, familyName, declaredFonts, filteredUsedFonts) {
-		let test;
-		const optionalUsedFonts = filteredUsedFonts && filteredUsedFonts.get(familyName);
-		if (optionalUsedFonts && optionalUsedFonts.length) {
-			let fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
-			if (VALID_FONT_STYLES.find(rule => fontStyle.trim().match(rule))) {
-				const fontWeight = singlefile.lib.helper.getFontWeight(getDeclarationValue(ruleData.block.children, "font-weight") || "400");
-				const declaredFontsWeights = declaredFonts
-					.filter(fontInfo => fontInfo.fontFamily == familyName && fontInfo.fontStyle == fontStyle)
-					.map(fontInfo => fontInfo.fontWeight)
-					.sort((weight1, weight2) => weight1 - weight2);
-				let usedFontWeights = optionalUsedFonts.map(fontInfo => getUsedFontWeight(fontInfo, fontStyle, declaredFontsWeights));
-				test = testFontweight(fontWeight, usedFontWeights);
-				if (!test) {
-					usedFontWeights = optionalUsedFonts.map(fontInfo => {
-						fontInfo = Array.from(fontInfo);
-						fontInfo[2] = "normal";
-						return getUsedFontWeight(fontInfo, fontStyle, declaredFontsWeights);
-					});
-				}
-				test = testFontweight(fontWeight, usedFontWeights);
-			} else {
-				test = true;
+function testUsedFont(ruleData, familyName, declaredFonts, filteredUsedFonts) {
+	let test;
+	const optionalUsedFonts = filteredUsedFonts && filteredUsedFonts.get(familyName);
+	if (optionalUsedFonts && optionalUsedFonts.length) {
+		let fontStyle = getDeclarationValue(ruleData.block.children, "font-style") || "normal";
+		if (VALID_FONT_STYLES.find(rule => fontStyle.trim().match(rule))) {
+			const fontWeight = helper.getFontWeight(getDeclarationValue(ruleData.block.children, "font-weight") || "400");
+			const declaredFontsWeights = declaredFonts
+				.filter(fontInfo => fontInfo.fontFamily == familyName && fontInfo.fontStyle == fontStyle)
+				.map(fontInfo => fontInfo.fontWeight)
+				.sort((weight1, weight2) => weight1 - weight2);
+			let usedFontWeights = optionalUsedFonts.map(fontInfo => getUsedFontWeight(fontInfo, fontStyle, declaredFontsWeights));
+			test = testFontweight(fontWeight, usedFontWeights);
+			if (!test) {
+				usedFontWeights = optionalUsedFonts.map(fontInfo => {
+					fontInfo = Array.from(fontInfo);
+					fontInfo[2] = "normal";
+					return getUsedFontWeight(fontInfo, fontStyle, declaredFontsWeights);
+				});
 			}
+			test = testFontweight(fontWeight, usedFontWeights);
 		} else {
 			test = true;
 		}
-		return test;
+	} else {
+		test = true;
 	}
+	return test;
+}
 
-	function testFontweight(fontWeight, usedFontWeights) {
-		return fontWeight.split(",").find(weightValue => usedFontWeights.includes(singlefile.lib.helper.getFontWeight(singlefile.lib.helper.removeQuotes(weightValue))));
-	}
+function testFontweight(fontWeight, usedFontWeights) {
+	return fontWeight.split(",").find(weightValue => usedFontWeights.includes(helper.getFontWeight(helper.removeQuotes(weightValue))));
+}
 
-	function getDeclarationValue(declarations, propertyName) {
-		let property;
-		if (declarations) {
-			property = declarations.filter(declaration => declaration.property == propertyName).tail;
-		}
-		if (property) {
-			try {
-				return singlefile.lib.helper.removeQuotes(singlefile.lib.vendor.cssTree.generate(property.data.value.toLowerCase()));
-			} catch (error) {
-				// ignored
-			}
+function getDeclarationValue(declarations, propertyName) {
+	let property;
+	if (declarations) {
+		property = declarations.filter(declaration => declaration.property == propertyName).tail;
+	}
+	if (property) {
+		try {
+			return helper.removeQuotes(cssTree.generate(property.data.value)).toLowerCase();
+		} catch (error) {
+			// ignored
 		}
 	}
+}
 
-	function getFontFamilyNames(declarations) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		let fontFamilyName = declarations.children.filter(node => node.property == "font-family").tail;
-		let fontFamilyNames = [];
-		if (fontFamilyName) {
-			let familyName = "";
-			if (fontFamilyName.data.value.children) {
-				fontFamilyName.data.value.children.forEach(node => {
-					if (node.type == "Operator" && node.value == "," && familyName) {
-						fontFamilyNames.push(singlefile.lib.helper.normalizeFontFamily(familyName));
-						familyName = "";
-					} else {
-						familyName += cssTree.generate(node);
-					}
-				});
-			} else {
-				fontFamilyName = cssTree.generate(fontFamilyName.data.value);
-			}
-			if (familyName) {
-				fontFamilyNames.push(singlefile.lib.helper.normalizeFontFamily(familyName));
-			}
+function getFontFamilyNames(declarations) {
+	let fontFamilyName = declarations.children.filter(node => node.property == "font-family").tail;
+	let fontFamilyNames = [];
+	if (fontFamilyName) {
+		let familyName = "";
+		if (fontFamilyName.data.value.children) {
+			fontFamilyName.data.value.children.forEach(node => {
+				if (node.type == "Operator" && node.value == "," && familyName) {
+					fontFamilyNames.push(helper.normalizeFontFamily(familyName));
+					familyName = "";
+				} else {
+					familyName += cssTree.generate(node);
+				}
+			});
+		} else {
+			fontFamilyName = cssTree.generate(fontFamilyName.data.value);
 		}
-		const font = declarations.children.filter(node => node.property == "font").tail;
-		if (font && font.data && font.data.value) {
-			try {
-				const parsedFont = singlefile.lib.vendor.fontPropertyParser.parse(cssTree.generate(font.data.value));
-				parsedFont.family.forEach(familyName => fontFamilyNames.push(singlefile.lib.helper.normalizeFontFamily(familyName)));
-			} catch (error) {
-				// ignored				
-			}
+		if (familyName) {
+			fontFamilyNames.push(helper.normalizeFontFamily(familyName));
+		}
+	}
+	const font = declarations.children.filter(node => node.property == "font").tail;
+	if (font && font.data && font.data.value) {
+		try {
+			const parsedFont = fontPropertyParser.parse(cssTree.generate(font.data.value));
+			parsedFont.family.forEach(familyName => fontFamilyNames.push(helper.normalizeFontFamily(familyName)));
+		} catch (error) {
+			// ignored				
 		}
-		return fontFamilyNames;
 	}
+	return fontFamilyNames;
+}
 
-	function getUsedFontWeight(fontInfo, fontStyle, fontWeights) {
-		let foundWeight;
-		if (fontInfo[2] == fontStyle) {
-			let fontWeight = Number(fontInfo[1]);
-			if (fontWeights.length > 1) {
-				if (fontWeight >= 400 && fontWeight <= 500) {
-					foundWeight = fontWeights.find(weight => weight >= fontWeight && weight <= 500);
-					if (!foundWeight) {
-						foundWeight = findDescendingFontWeight(fontWeight, fontWeights);
-					}
-					if (!foundWeight) {
-						foundWeight = findAscendingFontWeight(fontWeight, fontWeights);
-					}
+function getUsedFontWeight(fontInfo, fontStyle, fontWeights) {
+	let foundWeight;
+	if (fontInfo[2] == fontStyle) {
+		let fontWeight = Number(fontInfo[1]);
+		if (fontWeights.length > 1) {
+			if (fontWeight >= 400 && fontWeight <= 500) {
+				foundWeight = fontWeights.find(weight => weight >= fontWeight && weight <= 500);
+				if (!foundWeight) {
+					foundWeight = findDescendingFontWeight(fontWeight, fontWeights);
+				}
+				if (!foundWeight) {
+					foundWeight = findAscendingFontWeight(fontWeight, fontWeights);
 				}
-				if (fontWeight < 400) {
-					foundWeight = fontWeights.slice().reverse().find(weight => weight <= fontWeight);
-					if (!foundWeight) {
-						foundWeight = findAscendingFontWeight(fontWeight, fontWeights);
-					}
+			}
+			if (fontWeight < 400) {
+				foundWeight = fontWeights.slice().reverse().find(weight => weight <= fontWeight);
+				if (!foundWeight) {
+					foundWeight = findAscendingFontWeight(fontWeight, fontWeights);
 				}
-				if (fontWeight > 500) {
-					foundWeight = fontWeights.find(weight => weight >= fontWeight);
-					if (!foundWeight) {
-						foundWeight = findDescendingFontWeight(fontWeight, fontWeights);
-					}
+			}
+			if (fontWeight > 500) {
+				foundWeight = fontWeights.find(weight => weight >= fontWeight);
+				if (!foundWeight) {
+					foundWeight = findDescendingFontWeight(fontWeight, fontWeights);
 				}
-			} else {
-				foundWeight = fontWeights[0];
 			}
+		} else {
+			foundWeight = fontWeights[0];
 		}
-		return foundWeight;
 	}
+	return foundWeight;
+}
 
-	function findDescendingFontWeight(fontWeight, fontWeights) {
-		return fontWeights.slice().reverse().find(weight => weight < fontWeight);
-	}
+function findDescendingFontWeight(fontWeight, fontWeights) {
+	return fontWeights.slice().reverse().find(weight => weight < fontWeight);
+}
 
-	function findAscendingFontWeight(fontWeight, fontWeights) {
-		return fontWeights.find(weight => weight > fontWeight);
-	}
+function findAscendingFontWeight(fontWeight, fontWeights) {
+	return fontWeights.find(weight => weight > fontWeight);
+}
 
-	function getRulesTextContent(doc, cssRules, workStylesheet, content) {
-		cssRules.forEach(ruleData => {
-			if (ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
-				if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports")) {
-					content = getRulesTextContent(doc, ruleData.block.children, workStylesheet, content);
-				} else if (ruleData.type == "Rule") {
-					content = getDeclarationsTextContent(ruleData.block.children, workStylesheet, content);
-				}
+function getRulesTextContent(doc, cssRules, workStylesheet, content) {
+	cssRules.forEach(ruleData => {
+		if (ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
+			if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports")) {
+				content = getRulesTextContent(doc, ruleData.block.children, workStylesheet, content);
+			} else if (ruleData.type == "Rule") {
+				content = getDeclarationsTextContent(ruleData.block.children, workStylesheet, content);
 			}
-		});
-		return content;
-	}
-
-	function getDeclarationsTextContent(declarations, workStylesheet, content) {
-		const contentText = getDeclarationUnescapedValue(declarations, "content", workStylesheet);
-		const quotesText = getDeclarationUnescapedValue(declarations, "quotes", workStylesheet);
-		if (!content.includes(contentText)) {
-			content += contentText;
 		}
-		if (!content.includes(quotesText)) {
-			content += quotesText;
-		}
-		return content;
+	});
+	return content;
+}
+
+function getDeclarationsTextContent(declarations, workStylesheet, content) {
+	const contentText = getDeclarationUnescapedValue(declarations, "content", workStylesheet);
+	const quotesText = getDeclarationUnescapedValue(declarations, "quotes", workStylesheet);
+	if (!content.includes(contentText)) {
+		content += contentText;
 	}
+	if (!content.includes(quotesText)) {
+		content += quotesText;
+	}
+	return content;
+}
 
-	function getDeclarationUnescapedValue(declarations, property, workStylesheet) {
-		const rawValue = getDeclarationValue(declarations, property) || "";
-		if (rawValue) {
-			workStylesheet.textContent = "tmp { content:\"" + rawValue + "\"}";
-			if (workStylesheet.sheet && workStylesheet.sheet.cssRules) {
-				return singlefile.lib.helper.removeQuotes(workStylesheet.sheet.cssRules[0].style.getPropertyValue("content"));
-			} else {
-				return rawValue;
-			}
+function getDeclarationUnescapedValue(declarations, property, workStylesheet) {
+	const rawValue = getDeclarationValue(declarations, property) || "";
+	if (rawValue) {
+		workStylesheet.textContent = "tmp { content:\"" + rawValue + "\"}";
+		if (workStylesheet.sheet && workStylesheet.sheet.cssRules) {
+			return helper.removeQuotes(workStylesheet.sheet.cssRules[0].style.getPropertyValue("content"));
+		} else {
+			return rawValue;
 		}
-		return "";
 	}
+	return "";
+}
 
-	function testUnicodeRange(docContent, unicodeRange) {
-		if (unicodeRange) {
-			const unicodeRanges = unicodeRange.split(REGEXP_COMMA);
-			let invalid;
-			const result = unicodeRanges.filter(rangeValue => {
-				const range = rangeValue.split(REGEXP_DASH);
-				let regExpString;
-				if (range.length == 2) {
-					range[0] = transformRange(range[0]);
-					regExpString = "[" + range[0] + "-" + transformRange("U+" + range[1]) + "]";
-				}
-				if (range.length == 1) {
-					if (range[0].includes("?")) {
-						const firstRange = transformRange(range[0]);
-						const secondRange = firstRange;
-						regExpString = "[" + firstRange.replace(REGEXP_QUESTION_MARK, "0") + "-" + secondRange.replace(REGEXP_QUESTION_MARK, "F") + "]";
-					} else if (range[0]) {
-						regExpString = "[" + transformRange(range[0]) + "]";
-					}
+function testUnicodeRange(docContent, unicodeRange) {
+	if (unicodeRange) {
+		const unicodeRanges = unicodeRange.split(REGEXP_COMMA);
+		let invalid;
+		const result = unicodeRanges.filter(rangeValue => {
+			const range = rangeValue.split(REGEXP_DASH);
+			let regExpString;
+			if (range.length == 2) {
+				range[0] = transformRange(range[0]);
+				regExpString = "[" + range[0] + "-" + transformRange("U+" + range[1]) + "]";
+			}
+			if (range.length == 1) {
+				if (range[0].includes("?")) {
+					const firstRange = transformRange(range[0]);
+					const secondRange = firstRange;
+					regExpString = "[" + firstRange.replace(REGEXP_QUESTION_MARK, "0") + "-" + secondRange.replace(REGEXP_QUESTION_MARK, "F") + "]";
+				} else if (range[0]) {
+					regExpString = "[" + transformRange(range[0]) + "]";
 				}
-				if (regExpString) {
-					try {
-						return (new RegExp(regExpString, "u")).test(docContent);
-					} catch (error) {
-						invalid = true;
-						return false;
-					}
+			}
+			if (regExpString) {
+				try {
+					return (new RegExp(regExpString, "u")).test(docContent);
+				} catch (error) {
+					invalid = true;
+					return false;
 				}
-				return true;
-			});
-			return !invalid && (!unicodeRanges.length || result.length);
-		}
-		return true;
+			}
+			return true;
+		});
+		return !invalid && (!unicodeRanges.length || result.length);
 	}
+	return true;
+}
 
-	function transformRange(range) {
-		range = range.replace(REGEXP_STARTS_U_PLUS, "");
-		while (range.length < 6) {
-			range = "0" + range;
-		}
-		return "\\u{" + range + "}";
+function transformRange(range) {
+	range = range.replace(REGEXP_STARTS_U_PLUS, "");
+	while (range.length < 6) {
+		range = "0" + range;
 	}
-
-})(typeof globalThis == "object" ? globalThis : window);
+	return "\\u{" + range + "}";
+}

+ 272 - 279
lib/single-file/modules/css-matched-rules.js

@@ -21,337 +21,330 @@
  *   Source.
  */
 
-this.singlefile.lib.modules.matchedRules = this.singlefile.lib.modules.matchedRules || (() => {
+import * as cssTree from "./../vendor/css-tree.js";
 
-	const singlefile = this.singlefile;
+const MEDIA_ALL = "all";
+const IGNORED_PSEUDO_ELEMENTS = ["after", "before", "first-letter", "first-line", "placeholder", "selection", "part", "marker"];
+const SINGLE_FILE_HIDDEN_CLASS_NAME = "sf-hidden";
+const DISPLAY_STYLE = "display";
+const REGEXP_VENDOR_IDENTIFIER = /-(ms|webkit|moz|o)-/;
+const DEBUG = false;
 
-	const MEDIA_ALL = "all";
-	const IGNORED_PSEUDO_ELEMENTS = ["after", "before", "first-letter", "first-line", "placeholder", "selection", "part", "marker"];
-	const SINGLE_FILE_HIDDEN_CLASS_NAME = "sf-hidden";
-	const DISPLAY_STYLE = "display";
-	const REGEXP_VENDOR_IDENTIFIER = /-(ms|webkit|moz|o)-/;
-	const DEBUG = false;
-
-	class MatchedRules {
-		constructor(doc, stylesheets, styles) {
-			this.doc = doc;
-			this.mediaAllInfo = createMediaInfo(MEDIA_ALL);
-			const matchedElementsCache = new Map();
-			let sheetIndex = 0;
-			const workStyleElement = doc.createElement("style");
-			doc.body.appendChild(workStyleElement);
-			stylesheets.forEach(stylesheetInfo => {
-				if (!stylesheetInfo.scoped) {
-					const cssRules = stylesheetInfo.stylesheet.children;
-					if (cssRules) {
-						if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != MEDIA_ALL) {
-							const mediaInfo = createMediaInfo(stylesheetInfo.mediaText);
-							this.mediaAllInfo.medias.set("style-" + sheetIndex + "-" + stylesheetInfo.mediaText, mediaInfo);
-							getMatchedElementsRules(doc, cssRules, mediaInfo, sheetIndex, styles, matchedElementsCache, workStyleElement);
-						} else {
-							getMatchedElementsRules(doc, cssRules, this.mediaAllInfo, sheetIndex, styles, matchedElementsCache, workStyleElement);
-						}
+class MatchedRules {
+	constructor(doc, stylesheets, styles) {
+		this.doc = doc;
+		this.mediaAllInfo = createMediaInfo(MEDIA_ALL);
+		const matchedElementsCache = new Map();
+		let sheetIndex = 0;
+		const workStyleElement = doc.createElement("style");
+		doc.body.appendChild(workStyleElement);
+		stylesheets.forEach(stylesheetInfo => {
+			if (!stylesheetInfo.scoped) {
+				const cssRules = stylesheetInfo.stylesheet.children;
+				if (cssRules) {
+					if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != MEDIA_ALL) {
+						const mediaInfo = createMediaInfo(stylesheetInfo.mediaText);
+						this.mediaAllInfo.medias.set("style-" + sheetIndex + "-" + stylesheetInfo.mediaText, mediaInfo);
+						getMatchedElementsRules(doc, cssRules, mediaInfo, sheetIndex, styles, matchedElementsCache, workStyleElement);
+					} else {
+						getMatchedElementsRules(doc, cssRules, this.mediaAllInfo, sheetIndex, styles, matchedElementsCache, workStyleElement);
 					}
 				}
-				sheetIndex++;
-			});
-			let startTime;
-			if (DEBUG) {
-				startTime = Date.now();
-				log("  -- STARTED sortRules");
-			}
-			sortRules(this.mediaAllInfo);
-			if (DEBUG) {
-				log("  -- ENDED sortRules", Date.now() - startTime);
-				startTime = Date.now();
-				log("  -- STARTED computeCascade");
-			}
-			computeCascade(this.mediaAllInfo, [], this.mediaAllInfo, workStyleElement);
-			workStyleElement.remove();
-			if (DEBUG) {
-				log("  -- ENDED computeCascade", Date.now() - startTime);
 			}
+			sheetIndex++;
+		});
+		let startTime;
+		if (DEBUG) {
+			startTime = Date.now();
+			log("  -- STARTED sortRules");
 		}
-
-		getMediaAllInfo() {
-			return this.mediaAllInfo;
+		sortRules(this.mediaAllInfo);
+		if (DEBUG) {
+			log("  -- ENDED sortRules", Date.now() - startTime);
+			startTime = Date.now();
+			log("  -- STARTED computeCascade");
+		}
+		computeCascade(this.mediaAllInfo, [], this.mediaAllInfo, workStyleElement);
+		workStyleElement.remove();
+		if (DEBUG) {
+			log("  -- ENDED computeCascade", Date.now() - startTime);
 		}
 	}
 
-	return {
-		getMediaAllInfo
-	};
-
-	function getMediaAllInfo(doc, stylesheets, styles) {
-		return new MatchedRules(doc, stylesheets, styles).getMediaAllInfo();
+	getMediaAllInfo() {
+		return this.mediaAllInfo;
 	}
+}
 
-	function createMediaInfo(media) {
-		const mediaInfo = { media: media, elements: new Map(), medias: new Map(), rules: new Map(), pseudoRules: new Map() };
-		if (media == MEDIA_ALL) {
-			mediaInfo.matchedStyles = new Map();
-		}
-		return mediaInfo;
+export {
+	getMediaAllInfo
+};
+
+function getMediaAllInfo(doc, stylesheets, styles) {
+	return new MatchedRules(doc, stylesheets, styles).getMediaAllInfo();
+}
+
+function createMediaInfo(media) {
+	const mediaInfo = { media: media, elements: new Map(), medias: new Map(), rules: new Map(), pseudoRules: new Map() };
+	if (media == MEDIA_ALL) {
+		mediaInfo.matchedStyles = new Map();
 	}
+	return mediaInfo;
+}
 
-	function getMatchedElementsRules(doc, cssRules, mediaInfo, sheetIndex, styles, matchedElementsCache, workStylesheet) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		let mediaIndex = 0;
-		let ruleIndex = 0;
-		let startTime;
-		if (DEBUG && cssRules.length > 1) {
-			startTime = Date.now();
-			log("  -- STARTED getMatchedElementsRules", " index =", sheetIndex, "rules.length =", cssRules.length);
-		}
-		cssRules.forEach(ruleData => {
-			if (ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
-				if (ruleData.type == "Atrule" && ruleData.name == "media") {
-					const mediaText = cssTree.generate(ruleData.prelude);
-					const ruleMediaInfo = createMediaInfo(mediaText);
-					mediaInfo.medias.set("rule-" + sheetIndex + "-" + mediaIndex + "-" + mediaText, ruleMediaInfo);
-					mediaIndex++;
-					getMatchedElementsRules(doc, ruleData.block.children, ruleMediaInfo, sheetIndex, styles, matchedElementsCache, workStylesheet);
-				} else if (ruleData.type == "Rule") {
-					const selectors = ruleData.prelude.children.toArray();
-					const selectorsText = ruleData.prelude.children.toArray().map(selector => cssTree.generate(selector));
-					const ruleInfo = { ruleData, mediaInfo, ruleIndex, sheetIndex, matchedSelectors: new Set(), declarations: new Set(), selectors, selectorsText };
-					if (!invalidSelector(selectorsText.join(","), workStylesheet)) {
-						for (let selector = ruleData.prelude.children.head, selectorIndex = 0; selector; selector = selector.next, selectorIndex++) {
-							const selectorText = selectorsText[selectorIndex];
-							const selectorInfo = { selector, selectorText, ruleInfo };
-							getMatchedElementsSelector(doc, selectorInfo, styles, matchedElementsCache);
-						}
+function getMatchedElementsRules(doc, cssRules, mediaInfo, sheetIndex, styles, matchedElementsCache, workStylesheet) {
+	let mediaIndex = 0;
+	let ruleIndex = 0;
+	let startTime;
+	if (DEBUG && cssRules.length > 1) {
+		startTime = Date.now();
+		log("  -- STARTED getMatchedElementsRules", " index =", sheetIndex, "rules.length =", cssRules.length);
+	}
+	cssRules.forEach(ruleData => {
+		if (ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
+			if (ruleData.type == "Atrule" && ruleData.name == "media") {
+				const mediaText = cssTree.generate(ruleData.prelude);
+				const ruleMediaInfo = createMediaInfo(mediaText);
+				mediaInfo.medias.set("rule-" + sheetIndex + "-" + mediaIndex + "-" + mediaText, ruleMediaInfo);
+				mediaIndex++;
+				getMatchedElementsRules(doc, ruleData.block.children, ruleMediaInfo, sheetIndex, styles, matchedElementsCache, workStylesheet);
+			} else if (ruleData.type == "Rule") {
+				const selectors = ruleData.prelude.children.toArray();
+				const selectorsText = ruleData.prelude.children.toArray().map(selector => cssTree.generate(selector));
+				const ruleInfo = { ruleData, mediaInfo, ruleIndex, sheetIndex, matchedSelectors: new Set(), declarations: new Set(), selectors, selectorsText };
+				if (!invalidSelector(selectorsText.join(","), workStylesheet)) {
+					for (let selector = ruleData.prelude.children.head, selectorIndex = 0; selector; selector = selector.next, selectorIndex++) {
+						const selectorText = selectorsText[selectorIndex];
+						const selectorInfo = { selector, selectorText, ruleInfo };
+						getMatchedElementsSelector(doc, selectorInfo, styles, matchedElementsCache);
 					}
-					ruleIndex++;
 				}
+				ruleIndex++;
 			}
-		});
-		if (DEBUG && cssRules.length > 1) {
-			log("  -- ENDED   getMatchedElementsRules", "delay =", Date.now() - startTime);
 		}
+	});
+	if (DEBUG && cssRules.length > 1) {
+		log("  -- ENDED   getMatchedElementsRules", "delay =", Date.now() - startTime);
 	}
+}
 
-	function invalidSelector(selectorText, workStylesheet) {
-		workStylesheet.textContent = selectorText + "{}";
-		return workStylesheet.sheet ? !workStylesheet.sheet.cssRules.length : workStylesheet.sheet;
-	}
+function invalidSelector(selectorText, workStylesheet) {
+	workStylesheet.textContent = selectorText + "{}";
+	return workStylesheet.sheet ? !workStylesheet.sheet.cssRules.length : workStylesheet.sheet;
+}
 
-	function getMatchedElementsSelector(doc, selectorInfo, styles, matchedElementsCache) {
-		const filteredSelectorText = getFilteredSelector(selectorInfo.selector, selectorInfo.selectorText);
-		const selectorText = filteredSelectorText != selectorInfo.selectorText ? filteredSelectorText : selectorInfo.selectorText;
-		const cachedMatchedElements = matchedElementsCache.get(selectorText);
-		let matchedElements = cachedMatchedElements;
-		if (!matchedElements) {
-			try {
-				matchedElements = doc.querySelectorAll(selectorText);
-				if (selectorText != "." + SINGLE_FILE_HIDDEN_CLASS_NAME) {
-					matchedElements = Array.from(doc.querySelectorAll(selectorText)).filter(matchedElement =>
-						!matchedElement.classList.contains(SINGLE_FILE_HIDDEN_CLASS_NAME) &&
-						(matchedElement.style.getPropertyValue(DISPLAY_STYLE) != "none" || matchedElement.style.getPropertyPriority("display") != "important")
-					);
-				}
-			} catch (error) {
-				// ignored				
+function getMatchedElementsSelector(doc, selectorInfo, styles, matchedElementsCache) {
+	const filteredSelectorText = getFilteredSelector(selectorInfo.selector, selectorInfo.selectorText);
+	const selectorText = filteredSelectorText != selectorInfo.selectorText ? filteredSelectorText : selectorInfo.selectorText;
+	const cachedMatchedElements = matchedElementsCache.get(selectorText);
+	let matchedElements = cachedMatchedElements;
+	if (!matchedElements) {
+		try {
+			matchedElements = doc.querySelectorAll(selectorText);
+			if (selectorText != "." + SINGLE_FILE_HIDDEN_CLASS_NAME) {
+				matchedElements = Array.from(doc.querySelectorAll(selectorText)).filter(matchedElement =>
+					!matchedElement.classList.contains(SINGLE_FILE_HIDDEN_CLASS_NAME) &&
+					(matchedElement.style.getPropertyValue(DISPLAY_STYLE) != "none" || matchedElement.style.getPropertyPriority("display") != "important")
+				);
 			}
+		} catch (error) {
+			// ignored				
 		}
-		if (matchedElements) {
-			if (!cachedMatchedElements) {
-				matchedElementsCache.set(selectorText, matchedElements);
-			}
-			if (matchedElements.length) {
-				if (filteredSelectorText == selectorInfo.selectorText) {
-					matchedElements.forEach(element => addRule(element, selectorInfo, styles));
-				} else {
-					let pseudoSelectors = selectorInfo.ruleInfo.mediaInfo.pseudoRules.get(selectorInfo.ruleInfo.ruleData);
-					if (!pseudoSelectors) {
-						pseudoSelectors = new Set();
-						selectorInfo.ruleInfo.mediaInfo.pseudoRules.set(selectorInfo.ruleInfo.ruleData, pseudoSelectors);
-					}
-					pseudoSelectors.add(selectorInfo.selectorText);
+	}
+	if (matchedElements) {
+		if (!cachedMatchedElements) {
+			matchedElementsCache.set(selectorText, matchedElements);
+		}
+		if (matchedElements.length) {
+			if (filteredSelectorText == selectorInfo.selectorText) {
+				matchedElements.forEach(element => addRule(element, selectorInfo, styles));
+			} else {
+				let pseudoSelectors = selectorInfo.ruleInfo.mediaInfo.pseudoRules.get(selectorInfo.ruleInfo.ruleData);
+				if (!pseudoSelectors) {
+					pseudoSelectors = new Set();
+					selectorInfo.ruleInfo.mediaInfo.pseudoRules.set(selectorInfo.ruleInfo.ruleData, pseudoSelectors);
 				}
+				pseudoSelectors.add(selectorInfo.selectorText);
 			}
 		}
 	}
+}
 
-	function getFilteredSelector(selector, selectorText) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		const removedSelectors = [];
-		selector = { data: cssTree.parse(cssTree.generate(selector.data), { context: "selector" }) };
-		filterPseudoClasses(selector);
-		if (removedSelectors.length) {
-			removedSelectors.forEach(({ parentSelector, selector }) => {
-				if (parentSelector.data.children.getSize() == 0 || !selector.prev || selector.prev.data.type == "Combinator" || selector.prev.data.type == "WhiteSpace") {
-					parentSelector.data.children.replace(selector, cssTree.parse("*", { context: "selector" }).children.head);
-				} else {
-					parentSelector.data.children.remove(selector);
-				}
-			});
-			selectorText = cssTree.generate(selector.data).trim();
-		}
-		return selectorText;
-
-		function filterPseudoClasses(selector, parentSelector) {
-			if (selector.data.children) {
-				for (let childSelector = selector.data.children.head; childSelector; childSelector = childSelector.next) {
-					filterPseudoClasses(childSelector, selector);
-				}
+function getFilteredSelector(selector, selectorText) {
+	const removedSelectors = [];
+	selector = { data: cssTree.parse(cssTree.generate(selector.data), { context: "selector" }) };
+	filterPseudoClasses(selector);
+	if (removedSelectors.length) {
+		removedSelectors.forEach(({ parentSelector, selector }) => {
+			if (parentSelector.data.children.getSize() == 0 || !selector.prev || selector.prev.data.type == "Combinator" || selector.prev.data.type == "WhiteSpace") {
+				parentSelector.data.children.replace(selector, cssTree.parse("*", { context: "selector" }).children.head);
+			} else {
+				parentSelector.data.children.remove(selector);
 			}
-			if ((selector.data.type == "PseudoClassSelector") ||
-				(selector.data.type == "PseudoElementSelector" && (testVendorPseudo(selector) || IGNORED_PSEUDO_ELEMENTS.includes(selector.data.name)))) {
-				removedSelectors.push({ parentSelector, selector });
+		});
+		selectorText = cssTree.generate(selector.data).trim();
+	}
+	return selectorText;
+
+	function filterPseudoClasses(selector, parentSelector) {
+		if (selector.data.children) {
+			for (let childSelector = selector.data.children.head; childSelector; childSelector = childSelector.next) {
+				filterPseudoClasses(childSelector, selector);
 			}
 		}
-
-		function testVendorPseudo(selector) {
-			const name = selector.data.name;
-			return name.startsWith("-") || name.startsWith("\\-");
+		if ((selector.data.type == "PseudoClassSelector") ||
+			(selector.data.type == "PseudoElementSelector" && (testVendorPseudo(selector) || IGNORED_PSEUDO_ELEMENTS.includes(selector.data.name)))) {
+			removedSelectors.push({ parentSelector, selector });
 		}
 	}
 
-	function addRule(element, selectorInfo, styles) {
-		const mediaInfo = selectorInfo.ruleInfo.mediaInfo;
-		const elementStyle = styles.get(element);
-		let elementInfo = mediaInfo.elements.get(element);
-		if (!elementInfo) {
-			elementInfo = [];
-			if (elementStyle) {
-				elementInfo.push({ styleInfo: { styleData: elementStyle, declarations: new Set() } });
-			}
-			mediaInfo.elements.set(element, elementInfo);
+	function testVendorPseudo(selector) {
+		const name = selector.data.name;
+		return name.startsWith("-") || name.startsWith("\\-");
+	}
+}
+
+function addRule(element, selectorInfo, styles) {
+	const mediaInfo = selectorInfo.ruleInfo.mediaInfo;
+	const elementStyle = styles.get(element);
+	let elementInfo = mediaInfo.elements.get(element);
+	if (!elementInfo) {
+		elementInfo = [];
+		if (elementStyle) {
+			elementInfo.push({ styleInfo: { styleData: elementStyle, declarations: new Set() } });
 		}
-		const specificity = computeSpecificity(selectorInfo.selector.data);
-		specificity.ruleIndex = selectorInfo.ruleInfo.ruleIndex;
-		specificity.sheetIndex = selectorInfo.ruleInfo.sheetIndex;
-		selectorInfo.specificity = specificity;
-		elementInfo.push(selectorInfo);
+		mediaInfo.elements.set(element, elementInfo);
 	}
+	const specificity = computeSpecificity(selectorInfo.selector.data);
+	specificity.ruleIndex = selectorInfo.ruleInfo.ruleIndex;
+	specificity.sheetIndex = selectorInfo.ruleInfo.sheetIndex;
+	selectorInfo.specificity = specificity;
+	elementInfo.push(selectorInfo);
+}
 
-	function computeCascade(mediaInfo, parentMediaInfo, mediaAllInfo, workStylesheet) {
-		mediaInfo.elements.forEach((elementInfo/*, element*/) =>
-			getDeclarationsInfo(elementInfo, workStylesheet/*, element*/).forEach((declarationsInfo, property) => {
-				if (declarationsInfo.selectorInfo.ruleInfo || mediaInfo == mediaAllInfo) {
-					let info;
-					if (declarationsInfo.selectorInfo.ruleInfo) {
-						info = declarationsInfo.selectorInfo.ruleInfo;
-						const ruleData = info.ruleData;
-						const ascendantMedia = [mediaInfo, ...parentMediaInfo].find(media => media.rules.get(ruleData)) || mediaInfo;
-						ascendantMedia.rules.set(ruleData, info);
-						if (ruleData) {
-							info.matchedSelectors.add(declarationsInfo.selectorInfo.selectorText);
-						}
-					} else {
-						info = declarationsInfo.selectorInfo.styleInfo;
-						const styleData = info.styleData;
-						const matchedStyleInfo = mediaAllInfo.matchedStyles.get(styleData);
-						if (!matchedStyleInfo) {
-							mediaAllInfo.matchedStyles.set(styleData, info);
-						}
+function computeCascade(mediaInfo, parentMediaInfo, mediaAllInfo, workStylesheet) {
+	mediaInfo.elements.forEach((elementInfo/*, element*/) =>
+		getDeclarationsInfo(elementInfo, workStylesheet/*, element*/).forEach((declarationsInfo, property) => {
+			if (declarationsInfo.selectorInfo.ruleInfo || mediaInfo == mediaAllInfo) {
+				let info;
+				if (declarationsInfo.selectorInfo.ruleInfo) {
+					info = declarationsInfo.selectorInfo.ruleInfo;
+					const ruleData = info.ruleData;
+					const ascendantMedia = [mediaInfo, ...parentMediaInfo].find(media => media.rules.get(ruleData)) || mediaInfo;
+					ascendantMedia.rules.set(ruleData, info);
+					if (ruleData) {
+						info.matchedSelectors.add(declarationsInfo.selectorInfo.selectorText);
 					}
-					if (!info.declarations.has(property)) {
-						info.declarations.add(property);
+				} else {
+					info = declarationsInfo.selectorInfo.styleInfo;
+					const styleData = info.styleData;
+					const matchedStyleInfo = mediaAllInfo.matchedStyles.get(styleData);
+					if (!matchedStyleInfo) {
+						mediaAllInfo.matchedStyles.set(styleData, info);
 					}
 				}
-			}));
-		delete mediaInfo.elements;
-		mediaInfo.medias.forEach(childMediaInfo => computeCascade(childMediaInfo, [mediaInfo, ...parentMediaInfo], mediaAllInfo, workStylesheet));
-	}
-
-	function getDeclarationsInfo(elementInfo, workStylesheet/*, element*/) {
-		const declarationsInfo = new Map();
-		const processedProperties = new Set();
-		elementInfo.forEach(selectorInfo => {
-			let declarations;
-			if (selectorInfo.styleInfo) {
-				declarations = selectorInfo.styleInfo.styleData.children;
-			} else {
-				declarations = selectorInfo.ruleInfo.ruleData.block.children;
+				if (!info.declarations.has(property)) {
+					info.declarations.add(property);
+				}
 			}
-			processDeclarations(declarationsInfo, declarations, selectorInfo, processedProperties, workStylesheet);
-		});
-		return declarationsInfo;
-	}
+		}));
+	delete mediaInfo.elements;
+	mediaInfo.medias.forEach(childMediaInfo => computeCascade(childMediaInfo, [mediaInfo, ...parentMediaInfo], mediaAllInfo, workStylesheet));
+}
 
-	function processDeclarations(declarationsInfo, declarations, selectorInfo, processedProperties, workStylesheet) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		for (let declaration = declarations.tail; declaration; declaration = declaration.prev) {
-			const declarationData = declaration.data;
-			const declarationText = cssTree.generate(declarationData);
-			if (declarationData.type == "Declaration" &&
-				(declarationText.match(REGEXP_VENDOR_IDENTIFIER) || !processedProperties.has(declarationData.property) || declarationData.important) && !invalidDeclaration(declarationText, workStylesheet)) {
-				const declarationInfo = declarationsInfo.get(declarationData);
-				if (!declarationInfo || (declarationData.important && !declarationInfo.important)) {
-					declarationsInfo.set(declarationData, { selectorInfo, important: declarationData.important });
-					if (!declarationText.match(REGEXP_VENDOR_IDENTIFIER)) {
-						processedProperties.add(declarationData.property);
-					}
+function getDeclarationsInfo(elementInfo, workStylesheet/*, element*/) {
+	const declarationsInfo = new Map();
+	const processedProperties = new Set();
+	elementInfo.forEach(selectorInfo => {
+		let declarations;
+		if (selectorInfo.styleInfo) {
+			declarations = selectorInfo.styleInfo.styleData.children;
+		} else {
+			declarations = selectorInfo.ruleInfo.ruleData.block.children;
+		}
+		processDeclarations(declarationsInfo, declarations, selectorInfo, processedProperties, workStylesheet);
+	});
+	return declarationsInfo;
+}
+
+function processDeclarations(declarationsInfo, declarations, selectorInfo, processedProperties, workStylesheet) {
+	for (let declaration = declarations.tail; declaration; declaration = declaration.prev) {
+		const declarationData = declaration.data;
+		const declarationText = cssTree.generate(declarationData);
+		if (declarationData.type == "Declaration" &&
+			(declarationText.match(REGEXP_VENDOR_IDENTIFIER) || !processedProperties.has(declarationData.property) || declarationData.important) && !invalidDeclaration(declarationText, workStylesheet)) {
+			const declarationInfo = declarationsInfo.get(declarationData);
+			if (!declarationInfo || (declarationData.important && !declarationInfo.important)) {
+				declarationsInfo.set(declarationData, { selectorInfo, important: declarationData.important });
+				if (!declarationText.match(REGEXP_VENDOR_IDENTIFIER)) {
+					processedProperties.add(declarationData.property);
 				}
 			}
 		}
 	}
+}
 
-	function invalidDeclaration(declarationText, workStylesheet) {
-		let invalidDeclaration;
-		workStylesheet.textContent = "single-file-declaration { " + declarationText + "}";
-		if (workStylesheet.sheet && !workStylesheet.sheet.cssRules[0].style.length) {
-			if (!declarationText.match(REGEXP_VENDOR_IDENTIFIER)) {
-				invalidDeclaration = true;
-			}
+function invalidDeclaration(declarationText, workStylesheet) {
+	let invalidDeclaration;
+	workStylesheet.textContent = "single-file-declaration { " + declarationText + "}";
+	if (workStylesheet.sheet && !workStylesheet.sheet.cssRules[0].style.length) {
+		if (!declarationText.match(REGEXP_VENDOR_IDENTIFIER)) {
+			invalidDeclaration = true;
 		}
-		return invalidDeclaration;
 	}
+	return invalidDeclaration;
+}
 
-	function sortRules(media) {
-		media.elements.forEach(elementRules => elementRules.sort((ruleInfo1, ruleInfo2) =>
-			ruleInfo1.styleInfo && !ruleInfo2.styleInfo ? -1 :
-				!ruleInfo1.styleInfo && ruleInfo2.styleInfo ? 1 :
-					compareSpecificity(ruleInfo1.specificity, ruleInfo2.specificity)));
-		media.medias.forEach(sortRules);
-	}
+function sortRules(media) {
+	media.elements.forEach(elementRules => elementRules.sort((ruleInfo1, ruleInfo2) =>
+		ruleInfo1.styleInfo && !ruleInfo2.styleInfo ? -1 :
+			!ruleInfo1.styleInfo && ruleInfo2.styleInfo ? 1 :
+				compareSpecificity(ruleInfo1.specificity, ruleInfo2.specificity)));
+	media.medias.forEach(sortRules);
+}
 
-	function computeSpecificity(selector, specificity = { a: 0, b: 0, c: 0 }) {
-		if (selector.type == "IdSelector") {
-			specificity.a++;
-		}
-		if (selector.type == "ClassSelector" || selector.type == "AttributeSelector" || (selector.type == "PseudoClassSelector" && selector.name != "not")) {
-			specificity.b++;
-		}
-		if ((selector.type == "TypeSelector" && selector.name != "*") || selector.type == "PseudoElementSelector") {
-			specificity.c++;
-		}
-		if (selector.children) {
-			selector.children.forEach(selector => computeSpecificity(selector, specificity));
-		}
-		return specificity;
+function computeSpecificity(selector, specificity = { a: 0, b: 0, c: 0 }) {
+	if (selector.type == "IdSelector") {
+		specificity.a++;
 	}
-
-	function compareSpecificity(specificity1, specificity2) {
-		if (specificity1.a > specificity2.a) {
-			return -1;
-		} else if (specificity1.a < specificity2.a) {
-			return 1;
-		} else if (specificity1.b > specificity2.b) {
-			return -1;
-		} else if (specificity1.b < specificity2.b) {
-			return 1;
-		} else if (specificity1.c > specificity2.c) {
-			return -1;
-		} else if (specificity1.c < specificity2.c) {
-			return 1;
-		} else if (specificity1.sheetIndex > specificity2.sheetIndex) {
-			return -1;
-		} else if (specificity1.sheetIndex < specificity2.sheetIndex) {
-			return 1;
-		} else if (specificity1.ruleIndex > specificity2.ruleIndex) {
-			return -1;
-		} else if (specificity1.ruleIndex < specificity2.ruleIndex) {
-			return 1;
-		} else {
-			return -1;
-		}
+	if (selector.type == "ClassSelector" || selector.type == "AttributeSelector" || (selector.type == "PseudoClassSelector" && selector.name != "not")) {
+		specificity.b++;
+	}
+	if ((selector.type == "TypeSelector" && selector.name != "*") || selector.type == "PseudoElementSelector") {
+		specificity.c++;
+	}
+	if (selector.children) {
+		selector.children.forEach(selector => computeSpecificity(selector, specificity));
 	}
+	return specificity;
+}
 
-	function log(...args) {
-		console.log("S-File <css-mat>", ...args); // eslint-disable-line no-console
+function compareSpecificity(specificity1, specificity2) {
+	if (specificity1.a > specificity2.a) {
+		return -1;
+	} else if (specificity1.a < specificity2.a) {
+		return 1;
+	} else if (specificity1.b > specificity2.b) {
+		return -1;
+	} else if (specificity1.b < specificity2.b) {
+		return 1;
+	} else if (specificity1.c > specificity2.c) {
+		return -1;
+	} else if (specificity1.c < specificity2.c) {
+		return 1;
+	} else if (specificity1.sheetIndex > specificity2.sheetIndex) {
+		return -1;
+	} else if (specificity1.sheetIndex < specificity2.sheetIndex) {
+		return 1;
+	} else if (specificity1.ruleIndex > specificity2.ruleIndex) {
+		return -1;
+	} else if (specificity1.ruleIndex < specificity2.ruleIndex) {
+		return 1;
+	} else {
+		return -1;
 	}
+}
 
-})();
+function log(...args) {
+	console.log("S-File <css-mat>", ...args); // eslint-disable-line no-console
+}

+ 54 - 52
lib/single-file/modules/css-medias-alt-minifier.js

@@ -21,69 +21,71 @@
  *   Source.
  */
 
-this.singlefile.lib.modules.mediasAltMinifier = this.singlefile.lib.modules.mediasAltMinifier || (() => {
+import * as cssTree from "./../vendor/css-tree.js";
+import * as mediaQueryParser from "./../vendor/css-media-query-parser.js";
+import { flatten } from "./../single-file-helper.js";
 
-	const singlefile = this.singlefile;
+const helper = {
+	flatten
+};
 
-	const MEDIA_ALL = "all";
-	const MEDIA_SCREEN = "screen";
+const MEDIA_ALL = "all";
+const MEDIA_SCREEN = "screen";
 
-	return {
-		process
-	};
+export {
+	process
+};
 
-	function process(stylesheets) {
-		const stats = { processed: 0, discarded: 0 };
-		stylesheets.forEach((stylesheetInfo, element) => {
-			if (matchesMediaType(stylesheetInfo.mediaText || MEDIA_ALL, MEDIA_SCREEN) && stylesheetInfo.stylesheet.children) {
-				const removedRules = processRules(stylesheetInfo.stylesheet.children, stats);
-				removedRules.forEach(({ cssRules, cssRule }) => cssRules.remove(cssRule));
-			} else {
-				stylesheets.delete(element);
-			}
-		});
-		return stats;
-	}
+function process(stylesheets) {
+	const stats = { processed: 0, discarded: 0 };
+	stylesheets.forEach((stylesheetInfo, element) => {
+		if (matchesMediaType(stylesheetInfo.mediaText || MEDIA_ALL, MEDIA_SCREEN) && stylesheetInfo.stylesheet.children) {
+			const removedRules = processRules(stylesheetInfo.stylesheet.children, stats);
+			removedRules.forEach(({ cssRules, cssRule }) => cssRules.remove(cssRule));
+		} else {
+			stylesheets.delete(element);
+		}
+	});
+	return stats;
+}
 
-	function processRules(cssRules, stats, removedRules = []) {
-		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
-			const ruleData = cssRule.data;
-			if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
-				stats.processed++;
-				if (matchesMediaType(singlefile.lib.vendor.cssTree.generate(ruleData.prelude), MEDIA_SCREEN)) {
-					processRules(ruleData.block.children, stats, removedRules);
-				} else {
-					removedRules.push({ cssRules, cssRule });
-					stats.discarded++;
-				}
+function processRules(cssRules, stats, removedRules = []) {
+	for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
+		const ruleData = cssRule.data;
+		if (ruleData.type == "Atrule" && ruleData.name == "media" && ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
+			stats.processed++;
+			if (matchesMediaType(cssTree.generate(ruleData.prelude), MEDIA_SCREEN)) {
+				processRules(ruleData.block.children, stats, removedRules);
+			} else {
+				removedRules.push({ cssRules, cssRule });
+				stats.discarded++;
 			}
 		}
-		return removedRules;
 	}
+	return removedRules;
+}
 
-	function matchesMediaType(mediaText, mediaType) {
-		const foundMediaTypes = singlefile.lib.helper.flatten(singlefile.lib.vendor.mediaQueryParser.parseMediaList(mediaText).map(node => getMediaTypes(node, mediaType)));
-		return foundMediaTypes.find(mediaTypeInfo => (!mediaTypeInfo.not && (mediaTypeInfo.value == mediaType || mediaTypeInfo.value == MEDIA_ALL))
-			|| (mediaTypeInfo.not && (mediaTypeInfo.value == MEDIA_ALL || mediaTypeInfo.value != mediaType)));
-	}
+function matchesMediaType(mediaText, mediaType) {
+	const foundMediaTypes = helper.flatten(mediaQueryParser.parseMediaList(mediaText).map(node => getMediaTypes(node, mediaType)));
+	return foundMediaTypes.find(mediaTypeInfo => (!mediaTypeInfo.not && (mediaTypeInfo.value == mediaType || mediaTypeInfo.value == MEDIA_ALL))
+		|| (mediaTypeInfo.not && (mediaTypeInfo.value == MEDIA_ALL || mediaTypeInfo.value != mediaType)));
+}
 
-	function getMediaTypes(parentNode, mediaType, mediaTypes = []) {
-		parentNode.nodes.map((node, indexNode) => {
-			if (node.type == "media-query") {
-				return getMediaTypes(node, mediaType, mediaTypes);
-			} else {
-				if (node.type == "media-type") {
-					const nodeMediaType = { not: Boolean(indexNode && parentNode.nodes[0].type == "keyword" && parentNode.nodes[0].value == "not"), value: node.value };
-					if (!mediaTypes.find(mediaType => nodeMediaType.not == mediaType.not && nodeMediaType.value == mediaType.value)) {
-						mediaTypes.push(nodeMediaType);
-					}
+function getMediaTypes(parentNode, mediaType, mediaTypes = []) {
+	parentNode.nodes.map((node, indexNode) => {
+		if (node.type == "media-query") {
+			return getMediaTypes(node, mediaType, mediaTypes);
+		} else {
+			if (node.type == "media-type") {
+				const nodeMediaType = { not: Boolean(indexNode && parentNode.nodes[0].type == "keyword" && parentNode.nodes[0].value == "not"), value: node.value };
+				if (!mediaTypes.find(mediaType => nodeMediaType.not == mediaType.not && nodeMediaType.value == mediaType.value)) {
+					mediaTypes.push(nodeMediaType);
 				}
 			}
-		});
-		if (!mediaTypes.length) {
-			mediaTypes.push({ not: false, value: MEDIA_ALL });
 		}
-		return mediaTypes;
+	});
+	if (!mediaTypes.length) {
+		mediaTypes.push({ not: false, value: MEDIA_ALL });
 	}
-
-})();
+	return mediaTypes;
+}

+ 98 - 104
lib/single-file/modules/css-rules-minifier.js

@@ -21,132 +21,126 @@
  *   Source.
  */
 
-this.singlefile.lib.modules.cssRulesMinifier = this.singlefile.lib.modules.cssRulesMinifier || (() => {
+import * as cssTree from "./../vendor/css-tree.js";
 
-	const singlefile = this.singlefile;
+const DEBUG = false;
 
-	const DEBUG = false;
+export {
+	process
+};
 
-	return {
-		process
-	};
-
-	function process(stylesheets, styles, mediaAllInfo) {
-		const stats = { processed: 0, discarded: 0 };
-		let sheetIndex = 0;
-		stylesheets.forEach(stylesheetInfo => {
-			if (!stylesheetInfo.scoped) {
-				const cssRules = stylesheetInfo.stylesheet.children;
-				if (cssRules) {
-					stats.processed += cssRules.getSize();
-					stats.discarded += cssRules.getSize();
-					let mediaInfo;
-					if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != "all") {
-						mediaInfo = mediaAllInfo.medias.get("style-" + sheetIndex + "-" + stylesheetInfo.mediaText);
-					} else {
-						mediaInfo = mediaAllInfo;
-					}
-					processRules(cssRules, sheetIndex, mediaInfo);
-					stats.discarded -= cssRules.getSize();
+function process(stylesheets, styles, mediaAllInfo) {
+	const stats = { processed: 0, discarded: 0 };
+	let sheetIndex = 0;
+	stylesheets.forEach(stylesheetInfo => {
+		if (!stylesheetInfo.scoped) {
+			const cssRules = stylesheetInfo.stylesheet.children;
+			if (cssRules) {
+				stats.processed += cssRules.getSize();
+				stats.discarded += cssRules.getSize();
+				let mediaInfo;
+				if (stylesheetInfo.mediaText && stylesheetInfo.mediaText != "all") {
+					mediaInfo = mediaAllInfo.medias.get("style-" + sheetIndex + "-" + stylesheetInfo.mediaText);
+				} else {
+					mediaInfo = mediaAllInfo;
 				}
+				processRules(cssRules, sheetIndex, mediaInfo);
+				stats.discarded -= cssRules.getSize();
 			}
-			sheetIndex++;
-		});
-		let startTime;
-		if (DEBUG) {
-			startTime = Date.now();
-			log("  -- STARTED processStyleAttribute");
-		}
-		styles.forEach(style => processStyleAttribute(style, mediaAllInfo));
-		if (DEBUG) {
-			log("  -- ENDED   processStyleAttribute delay =", Date.now() - startTime);
 		}
-		return stats;
+		sheetIndex++;
+	});
+	let startTime;
+	if (DEBUG) {
+		startTime = Date.now();
+		log("  -- STARTED processStyleAttribute");
 	}
+	styles.forEach(style => processStyleAttribute(style, mediaAllInfo));
+	if (DEBUG) {
+		log("  -- ENDED   processStyleAttribute delay =", Date.now() - startTime);
+	}
+	return stats;
+}
 
-	function processRules(cssRules, sheetIndex, mediaInfo) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		let mediaRuleIndex = 0, startTime;
-		if (DEBUG && cssRules.getSize() > 1) {
-			startTime = Date.now();
-			log("  -- STARTED processRules", "rules.length =", cssRules.getSize());
-		}
-		const removedCssRules = [];
-		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
-			const ruleData = cssRule.data;
-			if (ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
-				if (ruleData.type == "Atrule" && ruleData.name == "media") {
-					const mediaText = cssTree.generate(ruleData.prelude);
-					processRules(ruleData.block.children, sheetIndex, mediaInfo.medias.get("rule-" + sheetIndex + "-" + mediaRuleIndex + "-" + mediaText));
+function processRules(cssRules, sheetIndex, mediaInfo) {
+	let mediaRuleIndex = 0, startTime;
+	if (DEBUG && cssRules.getSize() > 1) {
+		startTime = Date.now();
+		log("  -- STARTED processRules", "rules.length =", cssRules.getSize());
+	}
+	const removedCssRules = [];
+	for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
+		const ruleData = cssRule.data;
+		if (ruleData.block && ruleData.block.children && ruleData.prelude && ruleData.prelude.children) {
+			if (ruleData.type == "Atrule" && ruleData.name == "media") {
+				const mediaText = cssTree.generate(ruleData.prelude);
+				processRules(ruleData.block.children, sheetIndex, mediaInfo.medias.get("rule-" + sheetIndex + "-" + mediaRuleIndex + "-" + mediaText));
+				if (!ruleData.prelude.children.getSize() || !ruleData.block.children.getSize()) {
+					removedCssRules.push(cssRule);
+				}
+				mediaRuleIndex++;
+			} else if (ruleData.type == "Rule") {
+				const ruleInfo = mediaInfo.rules.get(ruleData);
+				const pseudoSelectors = mediaInfo.pseudoRules.get(ruleData);
+				if (!ruleInfo && !pseudoSelectors) {
+					removedCssRules.push(cssRule);
+				} else if (ruleInfo) {
+					processRuleInfo(ruleData, ruleInfo, pseudoSelectors);
 					if (!ruleData.prelude.children.getSize() || !ruleData.block.children.getSize()) {
 						removedCssRules.push(cssRule);
 					}
-					mediaRuleIndex++;
-				} else if (ruleData.type == "Rule") {
-					const ruleInfo = mediaInfo.rules.get(ruleData);
-					const pseudoSelectors = mediaInfo.pseudoRules.get(ruleData);
-					if (!ruleInfo && !pseudoSelectors) {
-						removedCssRules.push(cssRule);
-					} else if (ruleInfo) {
-						processRuleInfo(ruleData, ruleInfo, pseudoSelectors);
-						if (!ruleData.prelude.children.getSize() || !ruleData.block.children.getSize()) {
-							removedCssRules.push(cssRule);
-						}
-					}
-				}
-			} else {
-				if (!ruleData || ruleData.type == "Raw" || (ruleData.type == "Rule" && (!ruleData.prelude || ruleData.prelude.type == "Raw"))) {
-					removedCssRules.push(cssRule);
 				}
 			}
-		}
-		removedCssRules.forEach(cssRule => cssRules.remove(cssRule));
-		if (DEBUG && cssRules.getSize() > 1) {
-			log("  -- ENDED   processRules delay =", Date.now() - startTime);
+		} else {
+			if (!ruleData || ruleData.type == "Raw" || (ruleData.type == "Rule" && (!ruleData.prelude || ruleData.prelude.type == "Raw"))) {
+				removedCssRules.push(cssRule);
+			}
 		}
 	}
+	removedCssRules.forEach(cssRule => cssRules.remove(cssRule));
+	if (DEBUG && cssRules.getSize() > 1) {
+		log("  -- ENDED   processRules delay =", Date.now() - startTime);
+	}
+}
 
-	function processRuleInfo(ruleData, ruleInfo, pseudoSelectors) {
-		const cssTree = singlefile.lib.vendor.cssTree;
-		const removedDeclarations = [];
-		const removedSelectors = [];
-		let pseudoSelectorFound;
-		for (let selector = ruleData.prelude.children.head; selector; selector = selector.next) {
-			const selectorText = cssTree.generate(selector.data);
-			if (pseudoSelectors && pseudoSelectors.has(selectorText)) {
-				pseudoSelectorFound = true;
-			}
-			if (!ruleInfo.matchedSelectors.has(selectorText) && (!pseudoSelectors || !pseudoSelectors.has(selectorText))) {
-				removedSelectors.push(selector);
-			}
+function processRuleInfo(ruleData, ruleInfo, pseudoSelectors) {
+	const removedDeclarations = [];
+	const removedSelectors = [];
+	let pseudoSelectorFound;
+	for (let selector = ruleData.prelude.children.head; selector; selector = selector.next) {
+		const selectorText = cssTree.generate(selector.data);
+		if (pseudoSelectors && pseudoSelectors.has(selectorText)) {
+			pseudoSelectorFound = true;
 		}
-		if (!pseudoSelectorFound) {
-			for (let declaration = ruleData.block.children.tail; declaration; declaration = declaration.prev) {
-				if (!ruleInfo.declarations.has(declaration.data)) {
-					removedDeclarations.push(declaration);
-				}
-			}
+		if (!ruleInfo.matchedSelectors.has(selectorText) && (!pseudoSelectors || !pseudoSelectors.has(selectorText))) {
+			removedSelectors.push(selector);
 		}
-		removedDeclarations.forEach(declaration => ruleData.block.children.remove(declaration));
-		removedSelectors.forEach(selector => ruleData.prelude.children.remove(selector));
 	}
-
-	function processStyleAttribute(styleData, mediaAllInfo) {
-		const removedDeclarations = [];
-		const styleInfo = mediaAllInfo.matchedStyles.get(styleData);
-		if (styleInfo) {
-			let propertyFound;
-			for (let declaration = styleData.children.head; declaration && !propertyFound; declaration = declaration.next) {
-				if (!styleInfo.declarations.has(declaration.data)) {
-					removedDeclarations.push(declaration);
-				}
+	if (!pseudoSelectorFound) {
+		for (let declaration = ruleData.block.children.tail; declaration; declaration = declaration.prev) {
+			if (!ruleInfo.declarations.has(declaration.data)) {
+				removedDeclarations.push(declaration);
 			}
-			removedDeclarations.forEach(declaration => styleData.children.remove(declaration));
 		}
 	}
+	removedDeclarations.forEach(declaration => ruleData.block.children.remove(declaration));
+	removedSelectors.forEach(selector => ruleData.prelude.children.remove(selector));
+}
 
-	function log(...args) {
-		console.log("S-File <css-min>", ...args); // eslint-disable-line no-console
+function processStyleAttribute(styleData, mediaAllInfo) {
+	const removedDeclarations = [];
+	const styleInfo = mediaAllInfo.matchedStyles.get(styleData);
+	if (styleInfo) {
+		let propertyFound;
+		for (let declaration = styleData.children.head; declaration && !propertyFound; declaration = declaration.next) {
+			if (!styleInfo.declarations.has(declaration.data)) {
+				removedDeclarations.push(declaration);
+			}
+		}
+		removedDeclarations.forEach(declaration => styleData.children.remove(declaration));
 	}
+}
 
-})();
+function log(...args) {
+	console.log("S-File <css-min>", ...args); // eslint-disable-line no-console
+}

+ 64 - 68
lib/single-file/modules/html-images-alt-minifier.js

@@ -21,93 +21,89 @@
  *   Source.
  */
 
-this.singlefile.lib.modules.imagesAltMinifier = this.singlefile.lib.modules.imagesAltMinifier || (() => {
+import * as srcsetParser from "./../vendor/html-srcset-parser.js";
 
-	const singlefile = this.singlefile;
+const EMPTY_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
 
-	const EMPTY_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
+export {
+	process
+};
 
-	return {
-		process
-	};
-
-	function process(doc) {
-		doc.querySelectorAll("picture").forEach(pictureElement => {
-			const imgElement = pictureElement.querySelector("img");
-			if (imgElement) {
-				let { src, srcset } = getImgSrcData(imgElement);
-				if (!src) {
-					const data = getSourceSrcData(Array.from(pictureElement.querySelectorAll("source")).reverse());
-					src = data.src;
-					if (!srcset) {
-						srcset = data.srcset;
-					}
+function process(doc) {
+	doc.querySelectorAll("picture").forEach(pictureElement => {
+		const imgElement = pictureElement.querySelector("img");
+		if (imgElement) {
+			let { src, srcset } = getImgSrcData(imgElement);
+			if (!src) {
+				const data = getSourceSrcData(Array.from(pictureElement.querySelectorAll("source")).reverse());
+				src = data.src;
+				if (!srcset) {
+					srcset = data.srcset;
 				}
-				setSrc({ src, srcset }, imgElement, pictureElement);
 			}
-		});
-		doc.querySelectorAll(":not(picture) > img[srcset]").forEach(imgElement => setSrc(getImgSrcData(imgElement), imgElement));
+			setSrc({ src, srcset }, imgElement, pictureElement);
+		}
+	});
+	doc.querySelectorAll(":not(picture) > img[srcset]").forEach(imgElement => setSrc(getImgSrcData(imgElement), imgElement));
+}
+
+function getImgSrcData(imgElement) {
+	let src = imgElement.getAttribute("src");
+	if (src == EMPTY_IMAGE) {
+		src = null;
 	}
+	let srcset = getSourceSrc(imgElement.getAttribute("srcset"));
+	if (srcset == EMPTY_IMAGE) {
+		srcset = null;
+	}
+	return { src, srcset };
+}
 
-	function getImgSrcData(imgElement) {
-		let src = imgElement.getAttribute("src");
+function getSourceSrcData(sources) {
+	let source = sources.find(source => source.src);
+	let src = source && source.src;
+	let srcset = source && source.srcset;
+	if (!src) {
+		source = sources.find(source => getSourceSrc(source.src));
+		src = source && source.src;
 		if (src == EMPTY_IMAGE) {
 			src = null;
 		}
-		let srcset = getSourceSrc(imgElement.getAttribute("srcset"));
+	}
+	if (!srcset) {
+		source = sources.find(source => getSourceSrc(source.srcset));
+		srcset = source && source.srcset;
 		if (srcset == EMPTY_IMAGE) {
 			srcset = null;
 		}
-		return { src, srcset };
-	}
-
-	function getSourceSrcData(sources) {
-		let source = sources.find(source => source.src);
-		let src = source && source.src;
-		let srcset = source && source.srcset;
-		if (!src) {
-			source = sources.find(source => getSourceSrc(source.src));
-			src = source && source.src;
-			if (src == EMPTY_IMAGE) {
-				src = null;
-			}
-		}
-		if (!srcset) {
-			source = sources.find(source => getSourceSrc(source.srcset));
-			srcset = source && source.srcset;
-			if (srcset == EMPTY_IMAGE) {
-				srcset = null;
-			}
-		}
-		return { src, srcset };
 	}
+	return { src, srcset };
+}
 
-	function setSrc(srcData, imgElement, pictureElement) {
-		if (srcData.src) {
-			imgElement.setAttribute("src", srcData.src);
+function setSrc(srcData, imgElement, pictureElement) {
+	if (srcData.src) {
+		imgElement.setAttribute("src", srcData.src);
+		imgElement.setAttribute("srcset", "");
+		imgElement.setAttribute("sizes", "");
+	} else {
+		imgElement.setAttribute("src", EMPTY_IMAGE);
+		if (srcData.srcset) {
+			imgElement.setAttribute("srcset", srcData.srcset);
+		} else {
 			imgElement.setAttribute("srcset", "");
 			imgElement.setAttribute("sizes", "");
-		} else {
-			imgElement.setAttribute("src", EMPTY_IMAGE);
-			if (srcData.srcset) {
-				imgElement.setAttribute("srcset", srcData.srcset);
-			} else {
-				imgElement.setAttribute("srcset", "");
-				imgElement.setAttribute("sizes", "");
-			}
-		}
-		if (pictureElement) {
-			pictureElement.querySelectorAll("source").forEach(sourceElement => sourceElement.remove());
 		}
 	}
+	if (pictureElement) {
+		pictureElement.querySelectorAll("source").forEach(sourceElement => sourceElement.remove());
+	}
+}
 
-	function getSourceSrc(sourceSrcSet) {
-		if (sourceSrcSet) {
-			const srcset = singlefile.lib.vendor.srcsetParser.process(sourceSrcSet);
-			if (srcset.length) {
-				return (srcset.find(srcset => srcset.url)).url;
-			}
+function getSourceSrc(sourceSrcSet) {
+	if (sourceSrcSet) {
+		const srcset = srcsetParser.process(sourceSrcSet);
+		if (srcset.length) {
+			return (srcset.find(srcset => srcset.url)).url;
 		}
 	}
-
-})();
+}

+ 200 - 204
lib/single-file/modules/html-minifier.js

@@ -23,244 +23,240 @@
 
 // Derived from the work of Kirill Maltsev - https://github.com/posthtml/htmlnano
 
-this.singlefile.lib.modules.htmlMinifier = this.singlefile.lib.modules.htmlMinifier || (() => {
+// Source: https://github.com/kangax/html-minifier/issues/63
+const booleanAttributes = [
+	"allowfullscreen",
+	"async",
+	"autofocus",
+	"autoplay",
+	"checked",
+	"compact",
+	"controls",
+	"declare",
+	"default",
+	"defaultchecked",
+	"defaultmuted",
+	"defaultselected",
+	"defer",
+	"disabled",
+	"enabled",
+	"formnovalidate",
+	"hidden",
+	"indeterminate",
+	"inert",
+	"ismap",
+	"itemscope",
+	"loop",
+	"multiple",
+	"muted",
+	"nohref",
+	"noresize",
+	"noshade",
+	"novalidate",
+	"nowrap",
+	"open",
+	"pauseonexit",
+	"readonly",
+	"required",
+	"reversed",
+	"scoped",
+	"seamless",
+	"selected",
+	"sortable",
+	"truespeed",
+	"typemustmatch",
+	"visible"
+];
 
-	// Source: https://github.com/kangax/html-minifier/issues/63
-	const booleanAttributes = [
-		"allowfullscreen",
-		"async",
-		"autofocus",
-		"autoplay",
-		"checked",
-		"compact",
-		"controls",
-		"declare",
-		"default",
-		"defaultchecked",
-		"defaultmuted",
-		"defaultselected",
-		"defer",
-		"disabled",
-		"enabled",
-		"formnovalidate",
-		"hidden",
-		"indeterminate",
-		"inert",
-		"ismap",
-		"itemscope",
-		"loop",
-		"multiple",
-		"muted",
-		"nohref",
-		"noresize",
-		"noshade",
-		"novalidate",
-		"nowrap",
-		"open",
-		"pauseonexit",
-		"readonly",
-		"required",
-		"reversed",
-		"scoped",
-		"seamless",
-		"selected",
-		"sortable",
-		"truespeed",
-		"typemustmatch",
-		"visible"
-	];
+const noWhitespaceCollapseElements = ["script", "style", "pre", "textarea"];
 
-	const noWhitespaceCollapseElements = ["script", "style", "pre", "textarea"];
+// Source: https://www.w3.org/TR/html4/sgml/dtd.html#events (Generic Attributes)
+const safeToRemoveAttrs = [
+	"id",
+	"class",
+	"style",
+	"lang",
+	"dir",
+	"onclick",
+	"ondblclick",
+	"onmousedown",
+	"onmouseup",
+	"onmouseover",
+	"onmousemove",
+	"onmouseout",
+	"onkeypress",
+	"onkeydown",
+	"onkeyup"
+];
 
-	// Source: https://www.w3.org/TR/html4/sgml/dtd.html#events (Generic Attributes)
-	const safeToRemoveAttrs = [
-		"id",
-		"class",
-		"style",
-		"lang",
-		"dir",
-		"onclick",
-		"ondblclick",
-		"onmousedown",
-		"onmouseup",
-		"onmouseover",
-		"onmousemove",
-		"onmouseout",
-		"onkeypress",
-		"onkeydown",
-		"onkeyup"
-	];
-
-	const redundantAttributes = {
-		"form": {
-			"method": "get"
-		},
-		"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 redundantAttributes = {
+	"form": {
+		"method": "get"
+	},
+	"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 REGEXP_WHITESPACE = /[ \t\f\r]+/g;
-	const REGEXP_NEWLINE = /[\n]+/g;
-	const REGEXP_ENDS_WHITESPACE = /^\s+$/;
-	const NodeFilter_SHOW_ALL = 4294967295;
-	const Node_ELEMENT_NODE = 1;
-	const Node_TEXT_NODE = 3;
-	const Node_COMMENT_NODE = 8;
+const REGEXP_WHITESPACE = /[ \t\f\r]+/g;
+const REGEXP_NEWLINE = /[\n]+/g;
+const REGEXP_ENDS_WHITESPACE = /^\s+$/;
+const NodeFilter_SHOW_ALL = 4294967295;
+const Node_ELEMENT_NODE = 1;
+const Node_TEXT_NODE = 3;
+const Node_COMMENT_NODE = 8;
 
-	const modules = [
-		collapseBooleanAttributes,
-		mergeTextNodes,
-		collapseWhitespace,
-		removeComments,
-		removeEmptyAttributes,
-		removeRedundantAttributes,
-		compressJSONLD,
-		node => mergeElements(node, "style", (node, previousSibling) => node.parentElement && node.parentElement.tagName == "HEAD" && node.media == previousSibling.media && node.title == previousSibling.title)
-	];
+const modules = [
+	collapseBooleanAttributes,
+	mergeTextNodes,
+	collapseWhitespace,
+	removeComments,
+	removeEmptyAttributes,
+	removeRedundantAttributes,
+	compressJSONLD,
+	node => mergeElements(node, "style", (node, previousSibling) => node.parentElement && node.parentElement.tagName == "HEAD" && node.media == previousSibling.media && node.title == previousSibling.title)
+];
 
-	return {
-		process
-	};
+export {
+	process
+};
 
-	function process(doc, options) {
-		removeEmptyInlineElements(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, options));
-			const previousNode = node;
-			node = nodesWalker.nextNode();
-			if (deletedNode) {
-				previousNode.remove();
-			}
+function process(doc, options) {
+	removeEmptyInlineElements(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, options));
+		const previousNode = node;
+		node = nodesWalker.nextNode();
+		if (deletedNode) {
+			previousNode.remove();
 		}
 	}
+}
 
-	function collapseBooleanAttributes(node) {
-		if (node.nodeType == Node_ELEMENT_NODE) {
-			Array.from(node.attributes).forEach(attribute => {
-				if (booleanAttributes.includes(attribute.name)) {
-					node.setAttribute(attribute.name, "");
-				}
-			});
-		}
+function collapseBooleanAttributes(node) {
+	if (node.nodeType == Node_ELEMENT_NODE) {
+		Array.from(node.attributes).forEach(attribute => {
+			if (booleanAttributes.includes(attribute.name)) {
+				node.setAttribute(attribute.name, "");
+			}
+		});
 	}
+}
 
-	function mergeTextNodes(node) {
-		if (node.nodeType == Node_TEXT_NODE) {
-			if (node.previousSibling && node.previousSibling.nodeType == Node_TEXT_NODE) {
-				node.textContent = node.previousSibling.textContent + node.textContent;
-				node.previousSibling.remove();
-			}
+function mergeTextNodes(node) {
+	if (node.nodeType == Node_TEXT_NODE) {
+		if (node.previousSibling && node.previousSibling.nodeType == Node_TEXT_NODE) {
+			node.textContent = node.previousSibling.textContent + node.textContent;
+			node.previousSibling.remove();
 		}
 	}
+}
 
-	function mergeElements(node, tagName, acceptMerge) {
-		if (node.nodeType == Node_ELEMENT_NODE && node.tagName.toLowerCase() == tagName.toLowerCase()) {
-			let previousSibling = node.previousSibling;
-			const previousSiblings = [];
-			while (previousSibling && previousSibling.nodeType == Node_TEXT_NODE && !previousSibling.textContent.trim()) {
-				previousSiblings.push(previousSibling);
-				previousSibling = previousSibling.previousSibling;
-			}
-			if (previousSibling && previousSibling.nodeType == Node_ELEMENT_NODE && previousSibling.tagName == node.tagName && acceptMerge(node, previousSibling)) {
-				node.textContent = previousSibling.textContent + node.textContent;
-				previousSiblings.forEach(node => node.remove());
-				previousSibling.remove();
-			}
+function mergeElements(node, tagName, acceptMerge) {
+	if (node.nodeType == Node_ELEMENT_NODE && node.tagName.toLowerCase() == tagName.toLowerCase()) {
+		let previousSibling = node.previousSibling;
+		const previousSiblings = [];
+		while (previousSibling && previousSibling.nodeType == Node_TEXT_NODE && !previousSibling.textContent.trim()) {
+			previousSiblings.push(previousSibling);
+			previousSibling = previousSibling.previousSibling;
+		}
+		if (previousSibling && previousSibling.nodeType == Node_ELEMENT_NODE && previousSibling.tagName == node.tagName && acceptMerge(node, previousSibling)) {
+			node.textContent = previousSibling.textContent + node.textContent;
+			previousSiblings.forEach(node => node.remove());
+			previousSibling.remove();
 		}
 	}
+}
 
-	function collapseWhitespace(node, options) {
-		if (node.nodeType == Node_TEXT_NODE) {
-			let element = node.parentElement;
-			const spacePreserved = element.getAttribute(options.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME) == "";
-			if (!spacePreserved) {
-				const textContent = node.textContent;
-				let noWhitespace = noWhitespaceCollapse(element);
-				while (noWhitespace) {
-					element = element.parentElement;
-					noWhitespace = element && noWhitespaceCollapse(element);
-				}
-				if ((!element || noWhitespace) && textContent.length > 1) {
-					node.textContent = textContent.replace(REGEXP_WHITESPACE, getWhiteSpace(node)).replace(REGEXP_NEWLINE, "\n");
-				}
+function collapseWhitespace(node, options) {
+	if (node.nodeType == Node_TEXT_NODE) {
+		let element = node.parentElement;
+		const spacePreserved = element.getAttribute(options.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME) == "";
+		if (!spacePreserved) {
+			const textContent = node.textContent;
+			let noWhitespace = noWhitespaceCollapse(element);
+			while (noWhitespace) {
+				element = element.parentElement;
+				noWhitespace = element && noWhitespaceCollapse(element);
+			}
+			if ((!element || noWhitespace) && textContent.length > 1) {
+				node.textContent = textContent.replace(REGEXP_WHITESPACE, getWhiteSpace(node)).replace(REGEXP_NEWLINE, "\n");
 			}
 		}
 	}
+}
 
-	function getWhiteSpace(node) {
-		return node.parentElement && node.parentElement.tagName == "HEAD" ? "\n" : " ";
-	}
+function getWhiteSpace(node) {
+	return node.parentElement && node.parentElement.tagName == "HEAD" ? "\n" : " ";
+}
 
-	function noWhitespaceCollapse(element) {
-		return element && !noWhitespaceCollapseElements.includes(element.tagName.toLowerCase());
+function noWhitespaceCollapse(element) {
+	return element && !noWhitespaceCollapseElements.includes(element.tagName.toLowerCase());
+}
+
+function removeComments(node) {
+	if (node.nodeType == Node_COMMENT_NODE && node.parentElement.tagName != "HTML") {
+		return !node.textContent.toLowerCase().trim().startsWith("[if");
 	}
+}
 
-	function removeComments(node) {
-		if (node.nodeType == Node_COMMENT_NODE && node.parentElement.tagName != "HTML") {
-			return !node.textContent.toLowerCase().trim().startsWith("[if");
-		}
+function removeEmptyAttributes(node) {
+	if (node.nodeType == Node_ELEMENT_NODE) {
+		Array.from(node.attributes).forEach(attribute => {
+			if (safeToRemoveAttrs.includes(attribute.name.toLowerCase())) {
+				const attributeValue = node.getAttribute(attribute.name);
+				if (attributeValue == "" || (attributeValue || "").match(REGEXP_ENDS_WHITESPACE)) {
+					node.removeAttribute(attribute.name);
+				}
+			}
+		});
 	}
+}
 
-	function removeEmptyAttributes(node) {
-		if (node.nodeType == Node_ELEMENT_NODE) {
-			Array.from(node.attributes).forEach(attribute => {
-				if (safeToRemoveAttrs.includes(attribute.name.toLowerCase())) {
-					const attributeValue = node.getAttribute(attribute.name);
-					if (attributeValue == "" || (attributeValue || "").match(REGEXP_ENDS_WHITESPACE)) {
-						node.removeAttribute(attribute.name);
-					}
+function removeRedundantAttributes(node) {
+	if (node.nodeType == Node_ELEMENT_NODE) {
+		const tagRedundantAttributes = redundantAttributes[node.tagName.toLowerCase()];
+		if (tagRedundantAttributes) {
+			Object.keys(tagRedundantAttributes).forEach(redundantAttributeName => {
+				const tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
+				if (typeof tagRedundantAttributeValue == "function" ? tagRedundantAttributeValue(node) : node.getAttribute(redundantAttributeName) == tagRedundantAttributeValue) {
+					node.removeAttribute(redundantAttributeName);
 				}
 			});
 		}
 	}
+}
 
-	function removeRedundantAttributes(node) {
-		if (node.nodeType == Node_ELEMENT_NODE) {
-			const tagRedundantAttributes = redundantAttributes[node.tagName.toLowerCase()];
-			if (tagRedundantAttributes) {
-				Object.keys(tagRedundantAttributes).forEach(redundantAttributeName => {
-					const tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
-					if (typeof tagRedundantAttributeValue == "function" ? tagRedundantAttributeValue(node) : node.getAttribute(redundantAttributeName) == tagRedundantAttributeValue) {
-						node.removeAttribute(redundantAttributeName);
-					}
-				});
-			}
+function compressJSONLD(node) {
+	if (node.nodeType == Node_ELEMENT_NODE && node.tagName == "SCRIPT" && node.type == "application/ld+json" && node.textContent.trim()) {
+		try {
+			node.textContent = JSON.stringify(JSON.parse(node.textContent));
+		} catch (error) {
+			// ignored
 		}
 	}
+}
 
-	function compressJSONLD(node) {
-		if (node.nodeType == Node_ELEMENT_NODE && node.tagName == "SCRIPT" && node.type == "application/ld+json" && node.textContent.trim()) {
-			try {
-				node.textContent = JSON.stringify(JSON.parse(node.textContent));
-			} catch (error) {
-				// ignored
-			}
+function removeEmptyInlineElements(doc) {
+	doc.querySelectorAll("style, script:not([src])").forEach(element => {
+		if (!element.textContent.trim()) {
+			element.remove();
 		}
-	}
-
-	function removeEmptyInlineElements(doc) {
-		doc.querySelectorAll("style, script:not([src])").forEach(element => {
-			if (!element.textContent.trim()) {
-				element.remove();
-			}
-		});
-	}
-
-})();
+	});
+}

+ 136 - 140
lib/single-file/modules/html-serializer.js

@@ -21,164 +21,160 @@
  *   Source.
  */
 
-this.singlefile.lib.modules.serializer = this.singlefile.lib.modules.serializer || (() => {
+const SELF_CLOSED_TAG_NAMES = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"];
 
-	const SELF_CLOSED_TAG_NAMES = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"];
+const Node_ELEMENT_NODE = 1;
+const Node_TEXT_NODE = 3;
+const Node_COMMENT_NODE = 8;
 
-	const Node_ELEMENT_NODE = 1;
-	const Node_TEXT_NODE = 3;
-	const Node_COMMENT_NODE = 8;
+// see https://www.w3.org/TR/html5/syntax.html#optional-tags
+const OMITTED_START_TAGS = [
+	{ tagName: "head", accept: element => !element.childNodes.length || element.childNodes[0].nodeType == Node_ELEMENT_NODE },
+	{ tagName: "body", accept: element => !element.childNodes.length }
+];
+const OMITTED_END_TAGS = [
+	{ tagName: "html", accept: next => !next || next.nodeType != Node_COMMENT_NODE },
+	{ tagName: "head", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
+	{ tagName: "body", accept: next => !next || next.nodeType != Node_COMMENT_NODE },
+	{ tagName: "li", accept: (next, element) => (!next && element.parentElement && (element.parentElement.tagName == "UL" || element.parentElement.tagName == "OL")) || (next && ["LI"].includes(next.tagName)) },
+	{ tagName: "dt", accept: next => !next || ["DT", "DD"].includes(next.tagName) },
+	{ tagName: "p", accept: next => next && ["ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "DETAILS", "DIV", "DL", "FIELDSET", "FIGCAPTION", "FIGURE", "FOOTER", "FORM", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", "HR", "MAIN", "NAV", "OL", "P", "PRE", "SECTION", "TABLE", "UL"].includes(next.tagName) },
+	{ tagName: "dd", accept: next => !next || ["DT", "DD"].includes(next.tagName) },
+	{ tagName: "rt", accept: next => !next || ["RT", "RP"].includes(next.tagName) },
+	{ tagName: "rp", accept: next => !next || ["RT", "RP"].includes(next.tagName) },
+	{ tagName: "optgroup", accept: next => !next || ["OPTGROUP"].includes(next.tagName) },
+	{ tagName: "option", accept: next => !next || ["OPTION", "OPTGROUP"].includes(next.tagName) },
+	{ tagName: "colgroup", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
+	{ tagName: "caption", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
+	{ tagName: "thead", accept: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
+	{ tagName: "tbody", accept: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
+	{ tagName: "tfoot", accept: next => !next },
+	{ tagName: "tr", accept: next => !next || ["TR"].includes(next.tagName) },
+	{ tagName: "td", accept: next => !next || ["TD", "TH"].includes(next.tagName) },
+	{ tagName: "th", accept: next => !next || ["TD", "TH"].includes(next.tagName) }
+];
+const TEXT_NODE_TAGS = ["style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext", "noscript"];
 
-	// see https://www.w3.org/TR/html5/syntax.html#optional-tags
-	const OMITTED_START_TAGS = [
-		{ tagName: "head", accept: element => !element.childNodes.length || element.childNodes[0].nodeType == Node_ELEMENT_NODE },
-		{ tagName: "body", accept: element => !element.childNodes.length }
-	];
-	const OMITTED_END_TAGS = [
-		{ tagName: "html", accept: next => !next || next.nodeType != Node_COMMENT_NODE },
-		{ tagName: "head", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
-		{ tagName: "body", accept: next => !next || next.nodeType != Node_COMMENT_NODE },
-		{ tagName: "li", accept: (next, element) => (!next && element.parentElement && (element.parentElement.tagName == "UL" || element.parentElement.tagName == "OL")) || (next && ["LI"].includes(next.tagName)) },
-		{ tagName: "dt", accept: next => !next || ["DT", "DD"].includes(next.tagName) },
-		{ tagName: "p", accept: next => next && ["ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "DETAILS", "DIV", "DL", "FIELDSET", "FIGCAPTION", "FIGURE", "FOOTER", "FORM", "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", "HR", "MAIN", "NAV", "OL", "P", "PRE", "SECTION", "TABLE", "UL"].includes(next.tagName) },
-		{ tagName: "dd", accept: next => !next || ["DT", "DD"].includes(next.tagName) },
-		{ tagName: "rt", accept: next => !next || ["RT", "RP"].includes(next.tagName) },
-		{ tagName: "rp", accept: next => !next || ["RT", "RP"].includes(next.tagName) },
-		{ tagName: "optgroup", accept: next => !next || ["OPTGROUP"].includes(next.tagName) },
-		{ tagName: "option", accept: next => !next || ["OPTION", "OPTGROUP"].includes(next.tagName) },
-		{ tagName: "colgroup", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
-		{ tagName: "caption", accept: next => !next || (next.nodeType != Node_COMMENT_NODE && (next.nodeType != Node_TEXT_NODE || !startsWithSpaceChar(next.textContent))) },
-		{ tagName: "thead", accept: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
-		{ tagName: "tbody", accept: next => !next || ["TBODY", "TFOOT"].includes(next.tagName) },
-		{ tagName: "tfoot", accept: next => !next },
-		{ tagName: "tr", accept: next => !next || ["TR"].includes(next.tagName) },
-		{ tagName: "td", accept: next => !next || ["TD", "TH"].includes(next.tagName) },
-		{ tagName: "th", accept: next => !next || ["TD", "TH"].includes(next.tagName) }
-	];
-	const TEXT_NODE_TAGS = ["style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext", "noscript"];
+export {
+	process
+};
 
-	return {
-		process
-	};
-
-	function process(doc, compressHTML) {
-		const docType = doc.doctype;
-		let docTypeString = "";
-		if (docType) {
-			docTypeString = "<!DOCTYPE " + docType.nodeName;
-			if (docType.publicId) {
-				docTypeString += " PUBLIC \"" + docType.publicId + "\"";
-				if (docType.systemId)
-					docTypeString += " \"" + docType.systemId + "\"";
-			} else if (docType.systemId)
-				docTypeString += " SYSTEM \"" + docType.systemId + "\"";
-			if (docType.internalSubset)
-				docTypeString += " [" + docType.internalSubset + "]";
-			docTypeString += "> ";
-		}
-		return docTypeString + serialize(doc.documentElement, compressHTML);
+function process(doc, compressHTML) {
+	const docType = doc.doctype;
+	let docTypeString = "";
+	if (docType) {
+		docTypeString = "<!DOCTYPE " + docType.nodeName;
+		if (docType.publicId) {
+			docTypeString += " PUBLIC \"" + docType.publicId + "\"";
+			if (docType.systemId)
+				docTypeString += " \"" + docType.systemId + "\"";
+		} else if (docType.systemId)
+			docTypeString += " SYSTEM \"" + docType.systemId + "\"";
+		if (docType.internalSubset)
+			docTypeString += " [" + docType.internalSubset + "]";
+		docTypeString += "> ";
 	}
+	return docTypeString + serialize(doc.documentElement, 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, isSVG);
-		}
+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, isSVG);
 	}
+}
 
-	function serializeTextNode(textNode) {
-		const parentNode = textNode.parentNode;
-		let parentTagName;
-		if (parentNode && parentNode.nodeType == Node_ELEMENT_NODE) {
-			parentTagName = parentNode.tagName.toLowerCase();
-		}
-		if (!parentTagName || TEXT_NODE_TAGS.includes(parentTagName)) {
-			if (parentTagName == "script") {
-				return textNode.textContent.replace(/<\//gi, "<\\/").replace(/\/>/gi, "\\/>");
-			}
-			return textNode.textContent;
-		} else {
-			return textNode.textContent.replace(/&/g, "&amp;").replace(/\u00a0/g, "&nbsp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
+function serializeTextNode(textNode) {
+	const parentNode = textNode.parentNode;
+	let parentTagName;
+	if (parentNode && parentNode.nodeType == Node_ELEMENT_NODE) {
+		parentTagName = parentNode.tagName.toLowerCase();
+	}
+	if (!parentTagName || TEXT_NODE_TAGS.includes(parentTagName)) {
+		if (parentTagName == "script") {
+			return textNode.textContent.replace(/<\//gi, "<\\/").replace(/\/>/gi, "\\/>");
 		}
+		return textNode.textContent;
+	} else {
+		return textNode.textContent.replace(/&/g, "&amp;").replace(/\u00a0/g, "&nbsp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
 	}
+}
 
-	function serializeCommentNode(commentNode) {
-		return "<!--" + commentNode.textContent + "-->";
-	}
+function serializeCommentNode(commentNode) {
+	return "<!--" + commentNode.textContent + "-->";
+}
 
-	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 = "";
-		if (!omittedStartTag || element.attributes.length) {
-			content = "<" + tagName;
-			Array.from(element.attributes).forEach(attribute => content += serializeAttribute(attribute, element, compressHTML));
-			content += ">";
-		}
-		if (element.tagName == "TEMPLATE" && !element.childNodes.length) {
-			content += element.innerHTML;
-		} else {
-			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 (isSVG || (!omittedEndTag && !SELF_CLOSED_TAG_NAMES.includes(tagName))) {
-			content += "</" + tagName + ">";
-		}
-		return content;
+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 = "";
+	if (!omittedStartTag || element.attributes.length) {
+		content = "<" + tagName;
+		Array.from(element.attributes).forEach(attribute => content += serializeAttribute(attribute, element, compressHTML));
+		content += ">";
+	}
+	if (element.tagName == "TEMPLATE" && !element.childNodes.length) {
+		content += element.innerHTML;
+	} else {
+		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 (isSVG || (!omittedEndTag && !SELF_CLOSED_TAG_NAMES.includes(tagName))) {
+		content += "</" + tagName + ">";
 	}
+	return content;
+}
 
-	function serializeAttribute(attribute, element, compressHTML) {
-		const name = attribute.name;
-		let content = "";
-		if (!name.match(/["'>/=]/)) {
-			let value = attribute.value;
-			if (compressHTML && name == "class") {
-				value = Array.from(element.classList).map(className => className.trim()).join(" ");
+function serializeAttribute(attribute, element, compressHTML) {
+	const name = attribute.name;
+	let content = "";
+	if (!name.match(/["'>/=]/)) {
+		let value = attribute.value;
+		if (compressHTML && name == "class") {
+			value = Array.from(element.classList).map(className => className.trim()).join(" ");
+		}
+		let simpleQuotesValue;
+		value = value.replace(/&/g, "&amp;").replace(/\u00a0/g, "&nbsp;");
+		if (value.includes("\"")) {
+			if (value.includes("'") || !compressHTML) {
+				value = value.replace(/"/g, "&quot;");
+			} else {
+				simpleQuotesValue = true;
 			}
-			let simpleQuotesValue;
-			value = value.replace(/&/g, "&amp;").replace(/\u00a0/g, "&nbsp;");
-			if (value.includes("\"")) {
-				if (value.includes("'") || !compressHTML) {
-					value = value.replace(/"/g, "&quot;");
-				} else {
-					simpleQuotesValue = true;
-				}
+		}
+		const invalidUnquotedValue = !compressHTML || !value.match(/^[^ \t\n\f\r'"`=<>]+$/);
+		content += " ";
+		if (!attribute.namespace) {
+			content += name;
+		} else if (attribute.namespaceURI == "http://www.w3.org/XML/1998/namespace") {
+			content += "xml:" + name;
+		} else if (attribute.namespaceURI == "http://www.w3.org/2000/xmlns/") {
+			if (name !== "xmlns") {
+				content += "xmlns:";
 			}
-			const invalidUnquotedValue = !compressHTML || !value.match(/^[^ \t\n\f\r'"`=<>]+$/);
-			content += " ";
-			if (!attribute.namespace) {
-				content += name;
-			} else if (attribute.namespaceURI == "http://www.w3.org/XML/1998/namespace") {
-				content += "xml:" + name;
-			} else if (attribute.namespaceURI == "http://www.w3.org/2000/xmlns/") {
-				if (name !== "xmlns") {
-					content += "xmlns:";
-				}
-				content += name;
-			} else if (attribute.namespaceURI == "http://www.w3.org/1999/xlink") {
-				content += "xlink:" + name;
-			} else {
-				content += name;
+			content += name;
+		} else if (attribute.namespaceURI == "http://www.w3.org/1999/xlink") {
+			content += "xlink:" + name;
+		} else {
+			content += name;
+		}
+		if (value != "") {
+			content += "=";
+			if (invalidUnquotedValue) {
+				content += simpleQuotesValue ? "'" : "\"";
 			}
-			if (value != "") {
-				content += "=";
-				if (invalidUnquotedValue) {
-					content += simpleQuotesValue ? "'" : "\"";
-				}
-				content += value;
-				if (invalidUnquotedValue) {
-					content += simpleQuotesValue ? "'" : "\"";
-				}
+			content += value;
+			if (invalidUnquotedValue) {
+				content += simpleQuotesValue ? "'" : "\"";
 			}
 		}
-		return content;
-	}
-
-	function startsWithSpaceChar(textContent) {
-		return Boolean(textContent.match(/^[ \t\n\f\r]/));
 	}
+	return content;
+}
 
-})();
+function startsWithSpaceChar(textContent) {
+	return Boolean(textContent.match(/^[ \t\n\f\r]/));
+}

+ 42 - 0
lib/single-file/modules/index.js

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2010-2020 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or 
+ *   modify it under the terms of the GNU Affero General Public License 
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ * 
+ *   The code in this file is distributed in the hope that it will be useful, 
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may 
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+ *   AGPL normally required by section 4, provided you include this license 
+ *   notice and a URL through which recipients can access the Corresponding 
+ *   Source.
+ */
+
+import * as fontsAltMinifier from "./css-fonts-alt-minifier.js";
+import * as fontsMinifier from "./css-fonts-minifier";
+import * as matchedRules from "./css-matched-rules.js";
+import * as mediasAltMinifier from "./css-medias-alt-minifier.js";
+import * as cssRulesMinifier from "./css-rules-minifier.js";
+import * as imagesAltMinifier from "./html-images-alt-minifier.js";
+import * as htmlMinifier from "./html-minifier.js";
+import * as serializer from "./html-serializer.js";
+
+export {
+	fontsAltMinifier,
+	fontsMinifier,
+	matchedRules,
+	mediasAltMinifier,
+	cssRulesMinifier,
+	imagesAltMinifier,
+	htmlMinifier,
+	serializer
+};

+ 329 - 313
lib/single-file/processors/frame-tree/content/content-frame-tree.js

@@ -21,374 +21,390 @@
  *   Source.
  */
 
-/* global window, globalThis */
+/* global globalThis */
 
-this.singlefile.lib.processors.frameTree.content.frames = this.singlefile.lib.processors.frameTree.content.frames || (globalThis => {
+import * as lazy from "./../../lazy/content/content-lazy-loader.js";
+import {
+	ON_BEFORE_CAPTURE_EVENT_NAME,
+	ON_AFTER_CAPTURE_EVENT_NAME,
+	WIN_ID_ATTRIBUTE_NAME,
+	waitForUserScript,
+	preProcessDoc,
+	serialize,
+	postProcessDoc,
+	getShadowRoot
+} from "./../../../single-file-helper.js";
 
-	const singlefile = this.singlefile;
+const helper = {
+	ON_BEFORE_CAPTURE_EVENT_NAME,
+	ON_AFTER_CAPTURE_EVENT_NAME,
+	WIN_ID_ATTRIBUTE_NAME,
+	waitForUserScript,
+	preProcessDoc,
+	serialize,
+	postProcessDoc,
+	getShadowRoot
+};
 
-	const MESSAGE_PREFIX = "__frameTree__::";
-	const FRAMES_CSS_SELECTOR = "iframe, frame, object[type=\"text/html\"][data]";
-	const ALL_ELEMENTS_CSS_SELECTOR = "*";
-	const INIT_REQUEST_MESSAGE = "singlefile.frameTree.initRequest";
-	const ACK_INIT_REQUEST_MESSAGE = "singlefile.frameTree.ackInitRequest";
-	const CLEANUP_REQUEST_MESSAGE = "singlefile.frameTree.cleanupRequest";
-	const INIT_RESPONSE_MESSAGE = "singlefile.frameTree.initResponse";
-	const TARGET_ORIGIN = "*";
-	const TIMEOUT_INIT_REQUEST_MESSAGE = 750;
-	const TIMEOUT_INIT_RESPONSE_MESSAGE = 10000;
-	const TOP_WINDOW_ID = "0";
-	const WINDOW_ID_SEPARATOR = ".";
-	const TOP_WINDOW = globalThis.window == globalThis.top;
+const MESSAGE_PREFIX = "__frameTree__::";
+const FRAMES_CSS_SELECTOR = "iframe, frame, object[type=\"text/html\"][data]";
+const ALL_ELEMENTS_CSS_SELECTOR = "*";
+const INIT_REQUEST_MESSAGE = "singlefile.frameTree.initRequest";
+const ACK_INIT_REQUEST_MESSAGE = "singlefile.frameTree.ackInitRequest";
+const CLEANUP_REQUEST_MESSAGE = "singlefile.frameTree.cleanupRequest";
+const INIT_RESPONSE_MESSAGE = "singlefile.frameTree.initResponse";
+const TARGET_ORIGIN = "*";
+const TIMEOUT_INIT_REQUEST_MESSAGE = 750;
+const TIMEOUT_INIT_RESPONSE_MESSAGE = 10000;
+const TOP_WINDOW_ID = "0";
+const WINDOW_ID_SEPARATOR = ".";
+const TOP_WINDOW = globalThis.window == globalThis.top;
 
-	const browser = globalThis.browser;
-	const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
-	const top = globalThis.top;
-	const MessageChannel = globalThis.MessageChannel;
-	const document = globalThis.document;
+const browser = globalThis.browser;
+const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
+const top = globalThis.top;
+const MessageChannel = globalThis.MessageChannel;
+const document = globalThis.document;
 
-	const sessions = new Map();
-	let windowId;
-	if (TOP_WINDOW) {
-		windowId = TOP_WINDOW_ID;
-		if (browser && browser.runtime && browser.runtime.onMessage && browser.runtime.onMessage.addListener) {
-			browser.runtime.onMessage.addListener(message => {
-				if (message.method == INIT_RESPONSE_MESSAGE) {
-					initResponse(message);
-					return Promise.resolve({});
-				} else if (message.method == ACK_INIT_REQUEST_MESSAGE) {
-					clearFrameTimeout("requestTimeouts", message.sessionId, message.windowId);
-					createFrameResponseTimeout(message.sessionId, message.windowId);
-					return Promise.resolve({});
-				}
-			});
-		}
-	}
-	addEventListener("message", async event => {
-		if (typeof event.data == "string" && event.data.startsWith(MESSAGE_PREFIX)) {
-			event.preventDefault();
-			event.stopPropagation();
-			const message = JSON.parse(event.data.substring(MESSAGE_PREFIX.length));
-			if (message.method == INIT_REQUEST_MESSAGE) {
-				if (event.source) {
-					sendMessage(event.source, { method: ACK_INIT_REQUEST_MESSAGE, windowId: message.windowId, sessionId: message.sessionId });
-				}
-				if (!TOP_WINDOW) {
-					globalThis.stop();
-					if (message.options.loadDeferredImages && singlefile.lib.processors.lazy.content.loader) {
-						singlefile.lib.processors.lazy.content.loader.process(message.options);
-					}
-					await initRequestAsync(message);
-				}
+const sessions = new Map();
+let windowId;
+if (TOP_WINDOW) {
+	windowId = TOP_WINDOW_ID;
+	if (browser && browser.runtime && browser.runtime.onMessage && browser.runtime.onMessage.addListener) {
+		browser.runtime.onMessage.addListener(message => {
+			if (message.method == INIT_RESPONSE_MESSAGE) {
+				initResponse(message);
+				return Promise.resolve({});
 			} else if (message.method == ACK_INIT_REQUEST_MESSAGE) {
 				clearFrameTimeout("requestTimeouts", message.sessionId, message.windowId);
 				createFrameResponseTimeout(message.sessionId, message.windowId);
-			} else if (message.method == CLEANUP_REQUEST_MESSAGE) {
-				cleanupRequest(message);
-			} else if (message.method == INIT_RESPONSE_MESSAGE && sessions.get(message.sessionId)) {
-				const port = event.ports[0];
-				port.onmessage = event => initResponse(event.data);
+				return Promise.resolve({});
 			}
-		}
-	}, true);
-
-	return {
-		getAsync,
-		getSync,
-		cleanup,
-		initResponse,
-		TIMEOUT_INIT_REQUEST_MESSAGE
-	};
-
-	function getAsync(options) {
-		const sessionId = getNewSessionId();
-		options = JSON.parse(JSON.stringify(options));
-		return new Promise(resolve => {
-			sessions.set(sessionId, {
-				frames: [],
-				requestTimeouts: {},
-				responseTimeouts: {},
-				resolve: frames => {
-					frames.sessionId = sessionId;
-					resolve(frames);
-				}
-			});
-			initRequestAsync({ windowId, sessionId, options });
 		});
 	}
+}
+addEventListener("message", async event => {
+	if (typeof event.data == "string" && event.data.startsWith(MESSAGE_PREFIX)) {
+		event.preventDefault();
+		event.stopPropagation();
+		const message = JSON.parse(event.data.substring(MESSAGE_PREFIX.length));
+		if (message.method == INIT_REQUEST_MESSAGE) {
+			if (event.source) {
+				sendMessage(event.source, { method: ACK_INIT_REQUEST_MESSAGE, windowId: message.windowId, sessionId: message.sessionId });
+			}
+			if (!TOP_WINDOW) {
+				globalThis.stop();
+				if (message.options.loadDeferredImages) {
+					lazy.process(message.options);
+				}
+				await initRequestAsync(message);
+			}
+		} else if (message.method == ACK_INIT_REQUEST_MESSAGE) {
+			clearFrameTimeout("requestTimeouts", message.sessionId, message.windowId);
+			createFrameResponseTimeout(message.sessionId, message.windowId);
+		} else if (message.method == CLEANUP_REQUEST_MESSAGE) {
+			cleanupRequest(message);
+		} else if (message.method == INIT_RESPONSE_MESSAGE && sessions.get(message.sessionId)) {
+			const port = event.ports[0];
+			port.onmessage = event => initResponse(event.data);
+		}
+	}
+}, true);
+
+export {
+	getAsync,
+	getSync,
+	cleanup,
+	initResponse,
+	TIMEOUT_INIT_REQUEST_MESSAGE
+};
 
-	function getSync(options) {
-		const sessionId = getNewSessionId();
-		options = JSON.parse(JSON.stringify(options));
+function getAsync(options) {
+	const sessionId = getNewSessionId();
+	options = JSON.parse(JSON.stringify(options));
+	return new Promise(resolve => {
 		sessions.set(sessionId, {
 			frames: [],
 			requestTimeouts: {},
-			responseTimeouts: {}
+			responseTimeouts: {},
+			resolve: frames => {
+				frames.sessionId = sessionId;
+				resolve(frames);
+			}
 		});
-		initRequestSync({ windowId, sessionId, options });
-		const frames = sessions.get(sessionId).frames;
-		frames.sessionId = sessionId;
-		return frames;
-	}
+		initRequestAsync({ windowId, sessionId, options });
+	});
+}
 
-	function cleanup(sessionId) {
-		sessions.delete(sessionId);
-		cleanupRequest({ windowId, sessionId, options: { sessionId } });
-	}
+function getSync(options) {
+	const sessionId = getNewSessionId();
+	options = JSON.parse(JSON.stringify(options));
+	sessions.set(sessionId, {
+		frames: [],
+		requestTimeouts: {},
+		responseTimeouts: {}
+	});
+	initRequestSync({ windowId, sessionId, options });
+	const frames = sessions.get(sessionId).frames;
+	frames.sessionId = sessionId;
+	return frames;
+}
 
-	function getNewSessionId() {
-		return globalThis.crypto.getRandomValues(new Uint32Array(32)).join("");
-	}
+function cleanup(sessionId) {
+	sessions.delete(sessionId);
+	cleanupRequest({ windowId, sessionId, options: { sessionId } });
+}
+
+function getNewSessionId() {
+	return globalThis.crypto.getRandomValues(new Uint32Array(32)).join("");
+}
 
-	function initRequestSync(message) {
-		const waitForUserScript = singlefile.lib.helper.waitForUserScript;
-		const sessionId = message.sessionId;
-		if (!TOP_WINDOW) {
-			windowId = globalThis.frameId = message.windowId;
+function initRequestSync(message) {
+	const waitForUserScript = helper.waitForUserScript;
+	const sessionId = message.sessionId;
+	if (!TOP_WINDOW) {
+		windowId = globalThis.frameId = message.windowId;
+	}
+	processFrames(document, message.options, windowId, sessionId);
+	if (!TOP_WINDOW) {
+		if (message.options.userScriptEnabled && waitForUserScript.callback) {
+			waitForUserScript.callback(helper.ON_BEFORE_CAPTURE_EVENT_NAME);
 		}
-		processFrames(document, message.options, windowId, sessionId);
-		if (!TOP_WINDOW) {
-			if (message.options.userScriptEnabled && waitForUserScript) {
-				waitForUserScript(singlefile.lib.helper.ON_BEFORE_CAPTURE_EVENT_NAME);
-			}
-			sendInitResponse({ frames: [getFrameData(document, globalThis, windowId, message.options)], sessionId, requestedFrameId: document.documentElement.dataset.requestedFrameId && windowId });
-			if (message.options.userScriptEnabled && waitForUserScript) {
-				waitForUserScript(singlefile.lib.helper.ON_AFTER_CAPTURE_EVENT_NAME);
-			}
-			delete document.documentElement.dataset.requestedFrameId;
+		sendInitResponse({ frames: [getFrameData(document, globalThis, windowId, message.options)], sessionId, requestedFrameId: document.documentElement.dataset.requestedFrameId && windowId });
+		if (message.options.userScriptEnabled && waitForUserScript.callback) {
+			waitForUserScript.callback(helper.ON_AFTER_CAPTURE_EVENT_NAME);
 		}
+		delete document.documentElement.dataset.requestedFrameId;
 	}
+}
 
-	async function initRequestAsync(message) {
-		const waitForUserScript = singlefile.lib.helper.waitForUserScript;
-		const sessionId = message.sessionId;
-		if (!TOP_WINDOW) {
-			windowId = globalThis.frameId = message.windowId;
+async function initRequestAsync(message) {
+	const waitForUserScript = helper.waitForUserScript;
+	const sessionId = message.sessionId;
+	if (!TOP_WINDOW) {
+		windowId = globalThis.frameId = message.windowId;
+	}
+	processFrames(document, message.options, windowId, sessionId);
+	if (!TOP_WINDOW) {
+		if (message.options.userScriptEnabled && waitForUserScript.callback) {
+			await waitForUserScript.callback(helper.ON_BEFORE_CAPTURE_EVENT_NAME);
 		}
-		processFrames(document, message.options, windowId, sessionId);
-		if (!TOP_WINDOW) {
-			if (message.options.userScriptEnabled && waitForUserScript) {
-				await waitForUserScript(singlefile.lib.helper.ON_BEFORE_CAPTURE_EVENT_NAME);
-			}
-			sendInitResponse({ frames: [getFrameData(document, globalThis, windowId, message.options)], sessionId, requestedFrameId: document.documentElement.dataset.requestedFrameId && windowId });
-			if (message.options.userScriptEnabled && waitForUserScript) {
-				await waitForUserScript(singlefile.lib.helper.ON_AFTER_CAPTURE_EVENT_NAME);
-			}
-			delete document.documentElement.dataset.requestedFrameId;
+		sendInitResponse({ frames: [getFrameData(document, globalThis, windowId, message.options)], sessionId, requestedFrameId: document.documentElement.dataset.requestedFrameId && windowId });
+		if (message.options.userScriptEnabled && waitForUserScript.callback) {
+			await waitForUserScript.callback(helper.ON_AFTER_CAPTURE_EVENT_NAME);
 		}
+		delete document.documentElement.dataset.requestedFrameId;
 	}
+}
 
-	function cleanupRequest(message) {
-		const sessionId = message.sessionId;
-		cleanupFrames(getFrames(document), message.windowId, sessionId);
-	}
+function cleanupRequest(message) {
+	const sessionId = message.sessionId;
+	cleanupFrames(getFrames(document), message.windowId, sessionId);
+}
 
-	function initResponse(message) {
-		message.frames.forEach(frameData => clearFrameTimeout("responseTimeouts", message.sessionId, frameData.windowId));
-		const windowData = sessions.get(message.sessionId);
-		if (windowData) {
-			if (message.requestedFrameId) {
-				windowData.requestedFrameId = message.requestedFrameId;
+function initResponse(message) {
+	message.frames.forEach(frameData => clearFrameTimeout("responseTimeouts", message.sessionId, frameData.windowId));
+	const windowData = sessions.get(message.sessionId);
+	if (windowData) {
+		if (message.requestedFrameId) {
+			windowData.requestedFrameId = message.requestedFrameId;
+		}
+		message.frames.forEach(messageFrameData => {
+			let frameData = windowData.frames.find(frameData => messageFrameData.windowId == frameData.windowId);
+			if (!frameData) {
+				frameData = { windowId: messageFrameData.windowId };
+				windowData.frames.push(frameData);
 			}
-			message.frames.forEach(messageFrameData => {
-				let frameData = windowData.frames.find(frameData => messageFrameData.windowId == frameData.windowId);
-				if (!frameData) {
-					frameData = { windowId: messageFrameData.windowId };
-					windowData.frames.push(frameData);
-				}
-				if (!frameData.processed) {
-					frameData.content = messageFrameData.content;
-					frameData.baseURI = messageFrameData.baseURI;
-					frameData.title = messageFrameData.title;
-					frameData.canvases = messageFrameData.canvases;
-					frameData.fonts = messageFrameData.fonts;
-					frameData.stylesheets = messageFrameData.stylesheets;
-					frameData.images = messageFrameData.images;
-					frameData.posters = messageFrameData.posters;
-					frameData.usedFonts = messageFrameData.usedFonts;
-					frameData.shadowRoots = messageFrameData.shadowRoots;
-					frameData.imports = messageFrameData.imports;
-					frameData.processed = messageFrameData.processed;
-				}
-			});
-			const remainingFrames = windowData.frames.filter(frameData => !frameData.processed).length;
-			if (!remainingFrames) {
-				windowData.frames = windowData.frames.sort((frame1, frame2) => frame2.windowId.split(WINDOW_ID_SEPARATOR).length - frame1.windowId.split(WINDOW_ID_SEPARATOR).length);
-				if (windowData.resolve) {
-					if (windowData.requestedFrameId) {
-						windowData.frames.forEach(frameData => {
-							if (frameData.windowId == windowData.requestedFrameId) {
-								frameData.requestedFrame = true;
-							}
-						});
-					}
-					windowData.resolve(windowData.frames);
+			if (!frameData.processed) {
+				frameData.content = messageFrameData.content;
+				frameData.baseURI = messageFrameData.baseURI;
+				frameData.title = messageFrameData.title;
+				frameData.canvases = messageFrameData.canvases;
+				frameData.fonts = messageFrameData.fonts;
+				frameData.stylesheets = messageFrameData.stylesheets;
+				frameData.images = messageFrameData.images;
+				frameData.posters = messageFrameData.posters;
+				frameData.usedFonts = messageFrameData.usedFonts;
+				frameData.shadowRoots = messageFrameData.shadowRoots;
+				frameData.imports = messageFrameData.imports;
+				frameData.processed = messageFrameData.processed;
+			}
+		});
+		const remainingFrames = windowData.frames.filter(frameData => !frameData.processed).length;
+		if (!remainingFrames) {
+			windowData.frames = windowData.frames.sort((frame1, frame2) => frame2.windowId.split(WINDOW_ID_SEPARATOR).length - frame1.windowId.split(WINDOW_ID_SEPARATOR).length);
+			if (windowData.resolve) {
+				if (windowData.requestedFrameId) {
+					windowData.frames.forEach(frameData => {
+						if (frameData.windowId == windowData.requestedFrameId) {
+							frameData.requestedFrame = true;
+						}
+					});
 				}
+				windowData.resolve(windowData.frames);
 			}
 		}
 	}
-	function processFrames(doc, options, parentWindowId, sessionId) {
-		const frameElements = getFrames(doc);
-		processFramesAsync(doc, frameElements, options, parentWindowId, sessionId);
-		if (frameElements.length) {
-			processFramesSync(doc, frameElements, options, parentWindowId, sessionId);
-		}
+}
+function processFrames(doc, options, parentWindowId, sessionId) {
+	const frameElements = getFrames(doc);
+	processFramesAsync(doc, frameElements, options, parentWindowId, sessionId);
+	if (frameElements.length) {
+		processFramesSync(doc, frameElements, options, parentWindowId, sessionId);
 	}
+}
 
-	function processFramesAsync(doc, frameElements, options, parentWindowId, sessionId) {
-		const frames = [];
-		let requestTimeouts;
-		if (sessions.get(sessionId)) {
-			requestTimeouts = sessions.get(sessionId).requestTimeouts;
-		} else {
-			requestTimeouts = {};
-			sessions.set(sessionId, { requestTimeouts });
-		}
-		frameElements.forEach((frameElement, frameIndex) => {
-			const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
-			frameElement.setAttribute(singlefile.lib.helper.WIN_ID_ATTRIBUTE_NAME, windowId);
-			frames.push({ windowId });
-		});
-		sendInitResponse({ frames, sessionId, requestedFrameId: doc.documentElement.dataset.requestedFrameId && parentWindowId });
-		frameElements.forEach((frameElement, frameIndex) => {
-			const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
-			try {
-				sendMessage(frameElement.contentWindow, { method: INIT_REQUEST_MESSAGE, windowId, sessionId, options });
-			} catch (error) {
-				// ignored
-			}
-			requestTimeouts[windowId] = globalThis.setTimeout(() => sendInitResponse({ frames: [{ windowId, processed: true }], sessionId }), TIMEOUT_INIT_REQUEST_MESSAGE);
-		});
-		delete doc.documentElement.dataset.requestedFrameId;
+function processFramesAsync(doc, frameElements, options, parentWindowId, sessionId) {
+	const frames = [];
+	let requestTimeouts;
+	if (sessions.get(sessionId)) {
+		requestTimeouts = sessions.get(sessionId).requestTimeouts;
+	} else {
+		requestTimeouts = {};
+		sessions.set(sessionId, { requestTimeouts });
 	}
+	frameElements.forEach((frameElement, frameIndex) => {
+		const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
+		frameElement.setAttribute(helper.WIN_ID_ATTRIBUTE_NAME, windowId);
+		frames.push({ windowId });
+	});
+	sendInitResponse({ frames, sessionId, requestedFrameId: doc.documentElement.dataset.requestedFrameId && parentWindowId });
+	frameElements.forEach((frameElement, frameIndex) => {
+		const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
+		try {
+			sendMessage(frameElement.contentWindow, { method: INIT_REQUEST_MESSAGE, windowId, sessionId, options });
+		} catch (error) {
+			// ignored
+		}
+		requestTimeouts[windowId] = globalThis.setTimeout(() => sendInitResponse({ frames: [{ windowId, processed: true }], sessionId }), TIMEOUT_INIT_REQUEST_MESSAGE);
+	});
+	delete doc.documentElement.dataset.requestedFrameId;
+}
 
-	function processFramesSync(doc, frameElements, options, parentWindowId, sessionId) {
-		const frames = [];
-		frameElements.forEach((frameElement, frameIndex) => {
-			const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
-			let frameDoc;
+function processFramesSync(doc, frameElements, options, parentWindowId, sessionId) {
+	const frames = [];
+	frameElements.forEach((frameElement, frameIndex) => {
+		const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
+		let frameDoc;
+		try {
+			frameDoc = frameElement.contentDocument;
+		} catch (error) {
+			// ignored
+		}
+		if (frameDoc) {
 			try {
-				frameDoc = frameElement.contentDocument;
+				const frameWindow = frameElement.contentWindow;
+				frameWindow.stop();
+				clearFrameTimeout("requestTimeouts", sessionId, windowId);
+				processFrames(frameDoc, options, windowId, sessionId);
+				frames.push(getFrameData(frameDoc, frameWindow, windowId, options));
 			} catch (error) {
-				// ignored
-			}
-			if (frameDoc) {
-				try {
-					const frameWindow = frameElement.contentWindow;
-					frameWindow.stop();
-					clearFrameTimeout("requestTimeouts", sessionId, windowId);
-					processFrames(frameDoc, options, windowId, sessionId);
-					frames.push(getFrameData(frameDoc, frameWindow, windowId, options));
-				} catch (error) {
-					frames.push({ windowId, processed: true });
-				}
+				frames.push({ windowId, processed: true });
 			}
-		});
-		sendInitResponse({ frames, sessionId, requestedFrameId: doc.documentElement.dataset.requestedFrameId && parentWindowId });
-		delete doc.documentElement.dataset.requestedFrameId;
-	}
+		}
+	});
+	sendInitResponse({ frames, sessionId, requestedFrameId: doc.documentElement.dataset.requestedFrameId && parentWindowId });
+	delete doc.documentElement.dataset.requestedFrameId;
+}
 
-	function clearFrameTimeout(type, sessionId, windowId) {
-		const session = sessions.get(sessionId);
-		if (session && session[type]) {
-			const timeout = session[type][windowId];
-			if (timeout) {
-				globalThis.clearTimeout(timeout);
-				delete session[type][windowId];
-			}
+function clearFrameTimeout(type, sessionId, windowId) {
+	const session = sessions.get(sessionId);
+	if (session && session[type]) {
+		const timeout = session[type][windowId];
+		if (timeout) {
+			globalThis.clearTimeout(timeout);
+			delete session[type][windowId];
 		}
 	}
+}
 
-	function createFrameResponseTimeout(sessionId, windowId) {
-		const session = sessions.get(sessionId);
-		if (session && session.responseTimeouts) {
-			session.responseTimeouts[windowId] = globalThis.setTimeout(() => sendInitResponse({ frames: [{ windowId: windowId, processed: true }], sessionId: sessionId }), TIMEOUT_INIT_RESPONSE_MESSAGE);
-		}
+function createFrameResponseTimeout(sessionId, windowId) {
+	const session = sessions.get(sessionId);
+	if (session && session.responseTimeouts) {
+		session.responseTimeouts[windowId] = globalThis.setTimeout(() => sendInitResponse({ frames: [{ windowId: windowId, processed: true }], sessionId: sessionId }), TIMEOUT_INIT_RESPONSE_MESSAGE);
 	}
+}
 
-	function cleanupFrames(frameElements, parentWindowId, sessionId) {
-		frameElements.forEach((frameElement, frameIndex) => {
-			const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
-			frameElement.removeAttribute(singlefile.lib.helper.WIN_ID_ATTRIBUTE_NAME);
-			try {
-				sendMessage(frameElement.contentWindow, { method: CLEANUP_REQUEST_MESSAGE, windowId, sessionId });
-			} catch (error) {
-				// ignored
-			}
-		});
-		frameElements.forEach((frameElement, frameIndex) => {
-			const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
-			let frameDoc;
+function cleanupFrames(frameElements, parentWindowId, sessionId) {
+	frameElements.forEach((frameElement, frameIndex) => {
+		const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
+		frameElement.removeAttribute(helper.WIN_ID_ATTRIBUTE_NAME);
+		try {
+			sendMessage(frameElement.contentWindow, { method: CLEANUP_REQUEST_MESSAGE, windowId, sessionId });
+		} catch (error) {
+			// ignored
+		}
+	});
+	frameElements.forEach((frameElement, frameIndex) => {
+		const windowId = parentWindowId + WINDOW_ID_SEPARATOR + frameIndex;
+		let frameDoc;
+		try {
+			frameDoc = frameElement.contentDocument;
+		} catch (error) {
+			// ignored
+		}
+		if (frameDoc) {
 			try {
-				frameDoc = frameElement.contentDocument;
+				cleanupFrames(getFrames(frameDoc), windowId, sessionId);
 			} catch (error) {
 				// ignored
 			}
-			if (frameDoc) {
-				try {
-					cleanupFrames(getFrames(frameDoc), windowId, sessionId);
-				} catch (error) {
-					// ignored
-				}
-			}
-		});
-	}
-
-	function sendInitResponse(message) {
-		message.method = INIT_RESPONSE_MESSAGE;
-		try {
-			top.singlefile.lib.processors.frameTree.content.frames.initResponse(message);
-		} catch (error) {
-			sendMessage(top, message, true);
 		}
+	});
+}
+
+function sendInitResponse(message) {
+	message.method = INIT_RESPONSE_MESSAGE;
+	try {
+		top.frameTree.initResponse(message);
+	} catch (error) {
+		sendMessage(top, message, true);
 	}
+}
 
-	function sendMessage(targetWindow, message, useChannel) {
-		if (targetWindow == top && browser && browser.runtime && browser.runtime.sendMessage) {
-			browser.runtime.sendMessage(message);
+function sendMessage(targetWindow, message, useChannel) {
+	if (targetWindow == top && browser && browser.runtime && browser.runtime.sendMessage) {
+		browser.runtime.sendMessage(message);
+	} else {
+		if (useChannel) {
+			const channel = new MessageChannel();
+			targetWindow.postMessage(MESSAGE_PREFIX + JSON.stringify({ method: message.method, sessionId: message.sessionId }), TARGET_ORIGIN, [channel.port2]);
+			channel.port1.postMessage(message);
 		} else {
-			if (useChannel) {
-				const channel = new MessageChannel();
-				targetWindow.postMessage(MESSAGE_PREFIX + JSON.stringify({ method: message.method, sessionId: message.sessionId }), TARGET_ORIGIN, [channel.port2]);
-				channel.port1.postMessage(message);
-			} else {
-				targetWindow.postMessage(MESSAGE_PREFIX + JSON.stringify(message), TARGET_ORIGIN);
-			}
+			targetWindow.postMessage(MESSAGE_PREFIX + JSON.stringify(message), TARGET_ORIGIN);
 		}
 	}
+}
 
-	function getFrameData(document, globalThis, windowId, options) {
-		const helper = singlefile.lib.helper;
-		const docData = helper.preProcessDoc(document, globalThis, options);
-		const content = helper.serialize(document);
-		helper.postProcessDoc(document, docData.markedElements);
-		const baseURI = document.baseURI.split("#")[0];
-		return {
-			windowId,
-			content,
-			baseURI,
-			title: document.title,
-			canvases: docData.canvases,
-			fonts: docData.fonts,
-			stylesheets: docData.stylesheets,
-			images: docData.images,
-			posters: docData.posters,
-			usedFonts: docData.usedFonts,
-			shadowRoots: docData.shadowRoots,
-			imports: docData.imports,
-			processed: true
-		};
-	}
-
-	function getFrames(document) {
-		let frames = Array.from(document.querySelectorAll(FRAMES_CSS_SELECTOR));
-		document.querySelectorAll(ALL_ELEMENTS_CSS_SELECTOR).forEach(element => {
-			const shadowRoot = singlefile.lib.helper.getShadowRoot(element);
-			if (shadowRoot) {
-				frames = frames.concat(...shadowRoot.querySelectorAll(FRAMES_CSS_SELECTOR));
-			}
-		});
-		return frames;
-	}
+function getFrameData(document, globalThis, windowId, options) {
+	const docData = helper.preProcessDoc(document, globalThis, options);
+	const content = helper.serialize(document);
+	helper.postProcessDoc(document, docData.markedElements);
+	const baseURI = document.baseURI.split("#")[0];
+	return {
+		windowId,
+		content,
+		baseURI,
+		title: document.title,
+		canvases: docData.canvases,
+		fonts: docData.fonts,
+		stylesheets: docData.stylesheets,
+		images: docData.images,
+		posters: docData.posters,
+		usedFonts: docData.usedFonts,
+		shadowRoots: docData.shadowRoots,
+		imports: docData.imports,
+		processed: true
+	};
+}
 
-})(typeof globalThis == "object" ? globalThis : window);
+function getFrames(document) {
+	let frames = Array.from(document.querySelectorAll(FRAMES_CSS_SELECTOR));
+	document.querySelectorAll(ALL_ELEMENTS_CSS_SELECTOR).forEach(element => {
+		const shadowRoot = helper.getShadowRoot(element);
+		if (shadowRoot) {
+			frames = frames.concat(...shadowRoot.querySelectorAll(FRAMES_CSS_SELECTOR));
+		}
+	});
+	return frames;
+}

+ 127 - 131
lib/single-file/processors/hooks/content/content-hooks-frames.js

@@ -21,151 +21,147 @@
  *   Source.
  */
 
-/* global window, globalThis */
+/* global globalThis */
 
-this.singlefile.lib.processors.hooks.content.frames = this.singlefile.lib.processors.hooks.content.frames || (globalThis => {
+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_EVENT = "single-file-load-deferred-images-keep-zoom-level-reset";
+const LOAD_DEFERRED_IMAGES_RESET_EVENT = "single-file-load-deferred-images-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";
+const BLOCK_STORAGE_END_EVENT = "single-file-block-storage-end";
+const LOAD_IMAGE_EVENT = "single-file-load-image";
+const IMAGE_LOADED_EVENT = "single-file-image-loaded";
+const NEW_FONT_FACE_EVENT = "single-file-new-font-face";
 
-	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_EVENT = "single-file-load-deferred-images-keep-zoom-level-reset";
-	const LOAD_DEFERRED_IMAGES_RESET_EVENT = "single-file-load-deferred-images-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";
-	const BLOCK_STORAGE_END_EVENT = "single-file-block-storage-end";
-	const LOAD_IMAGE_EVENT = "single-file-load-image";
-	const IMAGE_LOADED_EVENT = "single-file-image-loaded";
-	const NEW_FONT_FACE_EVENT = "single-file-new-font-face";
-
-	const browser = globalThis.browser;
-	const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
-	const dispatchEvent = event => globalThis.dispatchEvent(event);
-	const CustomEvent = globalThis.CustomEvent;
-	const document = globalThis.document;
-	const HTMLDocument = globalThis.HTMLDocument;
-	const FileReader = globalThis.FileReader;
-	const Blob = globalThis.Blob;
+const browser = globalThis.browser;
+const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
+const dispatchEvent = event => globalThis.dispatchEvent(event);
+const CustomEvent = globalThis.CustomEvent;
+const document = globalThis.document;
+const HTMLDocument = globalThis.HTMLDocument;
+const FileReader = globalThis.FileReader;
+const Blob = globalThis.Blob;
 
-	const fontFaces = [];
+const fontFaces = [];
 
-	if (document instanceof HTMLDocument) {
-		if (browser && browser.runtime && browser.runtime.getURL) {
-			addEventListener(NEW_FONT_FACE_EVENT, event => {
-				const detail = event.detail;
-				if (!fontFaces.find(fontFace => JSON.stringify(fontFace) == JSON.stringify(detail))) {
-					fontFaces.push(event.detail);
-				}
-			});
-			let scriptElement = document.createElement("script");
-			scriptElement.textContent = "(" + injectedScript.toString() + ")()";
-			(document.documentElement || document).appendChild(scriptElement);
-			scriptElement.remove();
-			scriptElement = document.createElement("script");
-			scriptElement.src = browser.runtime.getURL("/lib/single-file/processors/hooks/content/content-hooks-frames-web.js");
-			scriptElement.async = false;
-			(document.documentElement || document).appendChild(scriptElement);
-			scriptElement.remove();
-		}
+if (document instanceof HTMLDocument) {
+	if (browser && browser.runtime && browser.runtime.getURL) {
+		addEventListener(NEW_FONT_FACE_EVENT, event => {
+			const detail = event.detail;
+			if (!fontFaces.find(fontFace => JSON.stringify(fontFace) == JSON.stringify(detail))) {
+				fontFaces.push(event.detail);
+			}
+		});
+		let scriptElement = document.createElement("script");
+		scriptElement.textContent = "(" + injectedScript.toString() + ")()";
+		(document.documentElement || document).appendChild(scriptElement);
+		scriptElement.remove();
+		scriptElement = document.createElement("script");
+		scriptElement.src = browser.runtime.getURL("/lib/single-file/processors/hooks/content/content-hooks-frames-web.js");
+		scriptElement.async = false;
+		(document.documentElement || document).appendChild(scriptElement);
+		scriptElement.remove();
 	}
+}
 
-	return {
-		getFontsData,
-		loadDeferredImagesStart,
-		loadDeferredImagesEnd,
-		loadDeferredImagesResetZoomLevel,
-		LOAD_IMAGE_EVENT,
-		IMAGE_LOADED_EVENT
-	};
+export {
+	getFontsData,
+	loadDeferredImagesStart,
+	loadDeferredImagesEnd,
+	loadDeferredImagesResetZoomLevel,
+	LOAD_IMAGE_EVENT,
+	IMAGE_LOADED_EVENT
+};
 
-	function getFontsData() {
-		return fontFaces;
-	}
+function getFontsData() {
+	return fontFaces;
+}
 
-	function loadDeferredImagesStart(options) {
-		if (options.loadDeferredImagesBlockCookies) {
-			dispatchEvent(new CustomEvent(BLOCK_COOKIES_START_EVENT));
-		}
-		if (options.loadDeferredImagesBlockStorage) {
-			dispatchEvent(new CustomEvent(BLOCK_STORAGE_START_EVENT));
-		}
-		if (options.loadDeferredImagesKeepZoomLevel) {
-			dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT));
-		} else {
-			dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_START_EVENT));
-		}
+function loadDeferredImagesStart(options) {
+	if (options.loadDeferredImagesBlockCookies) {
+		dispatchEvent(new CustomEvent(BLOCK_COOKIES_START_EVENT));
 	}
+	if (options.loadDeferredImagesBlockStorage) {
+		dispatchEvent(new CustomEvent(BLOCK_STORAGE_START_EVENT));
+	}
+	if (options.loadDeferredImagesKeepZoomLevel) {
+		dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT));
+	} else {
+		dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_START_EVENT));
+	}
+}
 
-	function loadDeferredImagesEnd(options) {
-		if (options.loadDeferredImagesBlockCookies) {
-			dispatchEvent(new CustomEvent(BLOCK_COOKIES_END_EVENT));
-		}
-		if (options.loadDeferredImagesBlockStorage) {
-			dispatchEvent(new CustomEvent(BLOCK_STORAGE_END_EVENT));
-		}
-		if (options.loadDeferredImagesKeepZoomLevel) {
-			dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT));
-		} else {
-			dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_END_EVENT));
-		}
+function loadDeferredImagesEnd(options) {
+	if (options.loadDeferredImagesBlockCookies) {
+		dispatchEvent(new CustomEvent(BLOCK_COOKIES_END_EVENT));
+	}
+	if (options.loadDeferredImagesBlockStorage) {
+		dispatchEvent(new CustomEvent(BLOCK_STORAGE_END_EVENT));
 	}
+	if (options.loadDeferredImagesKeepZoomLevel) {
+		dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT));
+	} else {
+		dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_END_EVENT));
+	}
+}
 
-	function loadDeferredImagesResetZoomLevel(options) {
-		if (options.loadDeferredImagesKeepZoomLevel) {
-			dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_EVENT));
-		} else {
-			dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_RESET_EVENT));
-		}
+function loadDeferredImagesResetZoomLevel(options) {
+	if (options.loadDeferredImagesKeepZoomLevel) {
+		dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_EVENT));
+	} else {
+		dispatchEvent(new CustomEvent(LOAD_DEFERRED_IMAGES_RESET_EVENT));
 	}
+}
 
-	function injectedScript() {
-		const console = globalThis.console;
-		const warn = (console && console.warn && ((...args) => console.warn(...args))) || (() => { });
-		const NEW_FONT_FACE_EVENT = "single-file-new-font-face";
-		const FONT_STYLE_PROPERTIES = {
-			family: "font-family",
-			style: "font-style",
-			weight: "font-weight",
-			stretch: "font-stretch",
-			unicodeRange: "unicode-range",
-			variant: "font-variant",
-			featureSettings: "font-feature-settings"
-		};
+function injectedScript() {
+	const console = globalThis.console;
+	const warn = (console && console.warn && ((...args) => console.warn(...args))) || (() => { });
+	const NEW_FONT_FACE_EVENT = "single-file-new-font-face";
+	const FONT_STYLE_PROPERTIES = {
+		family: "font-family",
+		style: "font-style",
+		weight: "font-weight",
+		stretch: "font-stretch",
+		unicodeRange: "unicode-range",
+		variant: "font-variant",
+		featureSettings: "font-feature-settings"
+	};
 
-		if (globalThis.FontFace) {
-			const FontFace = globalThis.FontFace;
-			let warningFontFaceDisplayed;
-			globalThis.FontFace = function () {
-				if (!warningFontFaceDisplayed) {
-					warn("SingleFile is hooking the FontFace constructor to get font URLs."); // eslint-disable-line no-console
-					warningFontFaceDisplayed = true;
-				}
-				const detail = {};
-				detail["font-family"] = arguments[0];
-				detail.src = arguments[1];
-				const descriptors = arguments[2];
-				if (descriptors) {
-					Object.keys(descriptors).forEach(descriptor => {
-						if (FONT_STYLE_PROPERTIES[descriptor]) {
-							detail[FONT_STYLE_PROPERTIES[descriptor]] = descriptors[descriptor];
-						}
-					});
-				}
-				if (detail.src instanceof ArrayBuffer) {
-					const reader = new FileReader();
-					reader.readAsDataURL(new Blob([detail.src]));
-					reader.addEventListener("load", () => {
-						detail.src = "url(" + reader.result + ")";
-						dispatchEvent(new CustomEvent(NEW_FONT_FACE_EVENT, { detail }));
-					});
-				} else {
+	if (globalThis.FontFace) {
+		const FontFace = globalThis.FontFace;
+		let warningFontFaceDisplayed;
+		globalThis.FontFace = function () {
+			if (!warningFontFaceDisplayed) {
+				warn("SingleFile is hooking the FontFace constructor to get font URLs."); // eslint-disable-line no-console
+				warningFontFaceDisplayed = true;
+			}
+			const detail = {};
+			detail["font-family"] = arguments[0];
+			detail.src = arguments[1];
+			const descriptors = arguments[2];
+			if (descriptors) {
+				Object.keys(descriptors).forEach(descriptor => {
+					if (FONT_STYLE_PROPERTIES[descriptor]) {
+						detail[FONT_STYLE_PROPERTIES[descriptor]] = descriptors[descriptor];
+					}
+				});
+			}
+			if (detail.src instanceof ArrayBuffer) {
+				const reader = new FileReader();
+				reader.readAsDataURL(new Blob([detail.src]));
+				reader.addEventListener("load", () => {
+					detail.src = "url(" + reader.result + ")";
 					dispatchEvent(new CustomEvent(NEW_FONT_FACE_EVENT, { detail }));
-				}
-				return new FontFace(...arguments);
-			};
-			globalThis.FontFace.toString = function () { return "function FontFace() { [native code] }"; };
-		}
+				});
+			} else {
+				dispatchEvent(new CustomEvent(NEW_FONT_FACE_EVENT, { detail }));
+			}
+			return new FontFace(...arguments);
+		};
+		globalThis.FontFace.toString = function () { return "function FontFace() { [native code] }"; };
 	}
-
-})(typeof globalThis == "object" ? globalThis : window);
+}

+ 12 - 17
lib/single-file/processors/hooks/content/content-hooks.js

@@ -21,24 +21,19 @@
  *   Source.
  */
 
-/* global window, globalThis */
+/* global globalThis */
 
-this.singlefile.lib.processors.hooks.content.main = this.singlefile.lib.processors.hooks.content.main || (globalThis => {
+const browser = globalThis.browser;
+const document = globalThis.document;
+const HTMLDocument = globalThis.HTMLDocument;
 
-	const browser = globalThis.browser;
-	const document = globalThis.document;
-	const HTMLDocument = globalThis.HTMLDocument;
-
-	if (document instanceof HTMLDocument) {
-		const scriptElement = document.createElement("script");
+if (document instanceof HTMLDocument) {
+	const scriptElement = document.createElement("script");
+	scriptElement.async = false;
+	if (browser && browser.runtime && browser.runtime.getURL) {
+		scriptElement.src = browser.runtime.getURL("/lib/single-file/processors/hooks/content/content-hooks-web.js");
 		scriptElement.async = false;
-		if (browser && browser.runtime && browser.runtime.getURL) {
-			scriptElement.src = browser.runtime.getURL("/lib/single-file/processors/hooks/content/content-hooks-web.js");
-			scriptElement.async = false;
-		}
-		(document.documentElement || document).appendChild(scriptElement);
-		scriptElement.remove();
 	}
-	return {};
-
-})(typeof globalThis == "object" ? globalThis : window);
+	(document.documentElement || document).appendChild(scriptElement);
+	scriptElement.remove();
+}

+ 34 - 0
lib/single-file/processors/index.js

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2020 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or 
+ *   modify it under the terms of the GNU Affero General Public License 
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ * 
+ *   The code in this file is distributed in the hope that it will be useful, 
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may 
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+ *   AGPL normally required by section 4, provided you include this license 
+ *   notice and a URL through which recipients can access the Corresponding 
+ *   Source.
+ */
+
+import * as frameTree from "./frame-tree/content/content-frame-tree.js";
+import * as hooks from "./hooks/content/content-hooks.js";
+import * as hooksFrames from "./hooks/content/content-hooks-frames.js";
+import * as lazy from "./lazy/content/content-lazy-loader.js";
+
+export {
+	frameTree,
+	hooks,
+	hooksFrames,
+	lazy
+};

+ 155 - 160
lib/single-file/processors/lazy/content/content-lazy-loader.js

@@ -21,182 +21,177 @@
  *   Source.
  */
 
-/* global window, globalThis */
-
-this.singlefile.lib.processors.lazy.content.loader = this.singlefile.lib.processors.lazy.content.loader || (globalThis => {
-
-	const ATTRIBUTES_MUTATION_TYPE = "attributes";
-
-	const singlefile = this.singlefile;
-	const browser = globalThis.browser;
-	const document = globalThis.document;
-	const MutationObserver = globalThis.MutationObserver;
-	const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
-	const removeEventListener = (type, listener, options) => globalThis.removeEventListener(type, listener, options);
-	const timeouts = new Map();
-
-	if (browser && browser.runtime && browser.runtime.onMessage && browser.runtime.onMessage.addListener) {
-		browser.runtime.onMessage.addListener(message => {
-			if (message.method == "singlefile.lazyTimeout.onTimeout") {
-				const timeoutData = timeouts.get(message.type);
-				if (timeoutData) {
-					timeouts.delete(message.type);
-					timeoutData.callback();
-				}
-			}
-		});
-	}
-
-	return {
-		process,
-		resetZoomLevel
-	};
-
-	async function process(options) {
-		if (document.documentElement) {
-			timeouts.clear();
-			const maxScrollY = Math.max(document.documentElement.scrollHeight - (document.documentElement.clientHeight * 1.5), 0);
-			const maxScrollX = Math.max(document.documentElement.scrollWidth - (document.documentElement.clientWidth * 1.5), 0);
-			if (globalThis.scrollY <= maxScrollY && globalThis.scrollX <= maxScrollX) {
-				return triggerLazyLoading(options);
+/* global globalThis */
+
+import * as hooksFrames from "./../../hooks/content/content-hooks-frames";
+import {
+	LAZY_SRC_ATTRIBUTE_NAME,
+	SINGLE_FILE_UI_ELEMENT_CLASS
+} from "./../../../single-file-helper.js";
+const helper = {
+	LAZY_SRC_ATTRIBUTE_NAME,
+	SINGLE_FILE_UI_ELEMENT_CLASS
+};
+
+const ATTRIBUTES_MUTATION_TYPE = "attributes";
+
+const browser = globalThis.browser;
+const document = globalThis.document;
+const MutationObserver = globalThis.MutationObserver;
+const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
+const removeEventListener = (type, listener, options) => globalThis.removeEventListener(type, listener, options);
+const timeouts = new Map();
+
+if (browser && browser.runtime && browser.runtime.onMessage && browser.runtime.onMessage.addListener) {
+	browser.runtime.onMessage.addListener(message => {
+		if (message.method == "singlefile.lazyTimeout.onTimeout") {
+			const timeoutData = timeouts.get(message.type);
+			if (timeoutData) {
+				timeouts.delete(message.type);
+				timeoutData.callback();
 			}
 		}
-	}
-
-	function resetZoomLevel(options) {
-		const frames = singlefile.lib.processors.hooks.content.frames;
-		if (frames) {
-			frames.loadDeferredImagesResetZoomLevel(options);
+	});
+}
+
+export {
+	process,
+	resetZoomLevel
+};
+
+async function process(options) {
+	if (document.documentElement) {
+		timeouts.clear();
+		const maxScrollY = Math.max(document.documentElement.scrollHeight - (document.documentElement.clientHeight * 1.5), 0);
+		const maxScrollX = Math.max(document.documentElement.scrollWidth - (document.documentElement.clientWidth * 1.5), 0);
+		if (globalThis.scrollY <= maxScrollY && globalThis.scrollX <= maxScrollX) {
+			return triggerLazyLoading(options);
 		}
 	}
-
-	function triggerLazyLoading(options) {
-		const frames = singlefile.lib.processors.hooks.content.frames;
-		return new Promise(async resolve => { // eslint-disable-line  no-async-promise-executor
-			let loadingImages;
-			const pendingImages = new Set();
-			const observer = new MutationObserver(async mutations => {
-				mutations = mutations.filter(mutation => mutation.type == ATTRIBUTES_MUTATION_TYPE);
-				if (mutations.length) {
-					const updated = mutations.filter(mutation => {
-						if (mutation.attributeName == "src") {
-							mutation.target.setAttribute(singlefile.lib.helper.LAZY_SRC_ATTRIBUTE_NAME, mutation.target.src);
-							mutation.target.addEventListener("load", onResourceLoad);
-						}
-						if (mutation.attributeName == "src" || mutation.attributeName == "srcset" || mutation.target.tagName == "SOURCE") {
-							return !mutation.target.classList || !mutation.target.classList.contains(singlefile.lib.helper.SINGLE_FILE_UI_ELEMENT_CLASS);
-						}
-					});
-					if (updated.length) {
-						loadingImages = true;
-						await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
-						if (!pendingImages.size) {
-							await deferLazyLoadEnd(observer, options, cleanupAndResolve);
-						}
+}
+
+function resetZoomLevel(options) {
+	hooksFrames.loadDeferredImagesResetZoomLevel(options);
+}
+
+function triggerLazyLoading(options) {
+	return new Promise(async resolve => { // eslint-disable-line  no-async-promise-executor
+		let loadingImages;
+		const pendingImages = new Set();
+		const observer = new MutationObserver(async mutations => {
+			mutations = mutations.filter(mutation => mutation.type == ATTRIBUTES_MUTATION_TYPE);
+			if (mutations.length) {
+				const updated = mutations.filter(mutation => {
+					if (mutation.attributeName == "src") {
+						mutation.target.setAttribute(helper.LAZY_SRC_ATTRIBUTE_NAME, mutation.target.src);
+						mutation.target.addEventListener("load", onResourceLoad);
+					}
+					if (mutation.attributeName == "src" || mutation.attributeName == "srcset" || mutation.target.tagName == "SOURCE") {
+						return !mutation.target.classList || !mutation.target.classList.contains(helper.SINGLE_FILE_UI_ELEMENT_CLASS);
+					}
+				});
+				if (updated.length) {
+					loadingImages = true;
+					await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
+					if (!pendingImages.size) {
+						await deferLazyLoadEnd(observer, options, cleanupAndResolve);
 					}
 				}
-			});
-			await setAsyncTimeout("idleTimeout", () => {
-				if (!loadingImages) {
-					clearAsyncTimeout("loadTimeout");
-					clearAsyncTimeout("maxTimeout");
-					lazyLoadEnd(observer, options, cleanupAndResolve);
-				}
-			}, options.loadDeferredImagesMaxIdleTime * 2);
-			await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
-			observer.observe(document, { subtree: true, childList: true, attributes: true });
-			if (frames) {
-				addEventListener(frames.LOAD_IMAGE_EVENT, onImageLoadEvent);
-				addEventListener(frames.IMAGE_LOADED_EVENT, onImageLoadedEvent);
-				frames.loadDeferredImagesStart(options);
 			}
-
-			function onResourceLoad(event) {
-				const element = event.target;
-				element.removeAttribute(singlefile.lib.helper.LAZY_SRC_ATTRIBUTE_NAME);
-				element.removeEventListener("load", onResourceLoad);
+		});
+		await setAsyncTimeout("idleTimeout", () => {
+			if (!loadingImages) {
+				clearAsyncTimeout("loadTimeout");
+				clearAsyncTimeout("maxTimeout");
+				lazyLoadEnd(observer, options, cleanupAndResolve);
 			}
+		}, options.loadDeferredImagesMaxIdleTime * 2);
+		await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
+		observer.observe(document, { subtree: true, childList: true, attributes: true });
+		addEventListener(hooksFrames.LOAD_IMAGE_EVENT, onImageLoadEvent);
+		addEventListener(hooksFrames.IMAGE_LOADED_EVENT, onImageLoadedEvent);
+		hooksFrames.loadDeferredImagesStart(options);
+
+		function onResourceLoad(event) {
+			const element = event.target;
+			element.removeAttribute(helper.LAZY_SRC_ATTRIBUTE_NAME);
+			element.removeEventListener("load", onResourceLoad);
+		}
 
-			async function onImageLoadEvent(event) {
-				loadingImages = true;
-				await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
-				await deferLazyLoadEnd(observer, options, cleanupAndResolve);
-				if (event.detail) {
-					pendingImages.add(event.detail);
-				}
+		async function onImageLoadEvent(event) {
+			loadingImages = true;
+			await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
+			await deferLazyLoadEnd(observer, options, cleanupAndResolve);
+			if (event.detail) {
+				pendingImages.add(event.detail);
 			}
+		}
 
-			async function onImageLoadedEvent(event) {
-				await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
+		async function onImageLoadedEvent(event) {
+			await deferForceLazyLoadEnd(observer, options, cleanupAndResolve);
+			await deferLazyLoadEnd(observer, options, cleanupAndResolve);
+			pendingImages.delete(event.detail);
+			if (!pendingImages.size) {
 				await deferLazyLoadEnd(observer, options, cleanupAndResolve);
-				pendingImages.delete(event.detail);
-				if (!pendingImages.size) {
-					await deferLazyLoadEnd(observer, options, cleanupAndResolve);
-				}
 			}
-
-			function cleanupAndResolve(value) {
-				observer.disconnect();
-				if (frames) {
-					removeEventListener(frames.LOAD_IMAGE_EVENT, onImageLoadEvent);
-					removeEventListener(frames.IMAGE_LOADED_EVENT, onImageLoadedEvent);
-				}
-				resolve(value);
-			}
-		});
-	}
-
-	async function deferLazyLoadEnd(observer, options, resolve) {
-		await setAsyncTimeout("loadTimeout", () => lazyLoadEnd(observer, options, resolve), options.loadDeferredImagesMaxIdleTime);
-	}
-
-	async function deferForceLazyLoadEnd(observer, options, resolve) {
-		await setAsyncTimeout("maxTimeout", async () => {
-			await clearAsyncTimeout("loadTimeout");
-			await lazyLoadEnd(observer, options, resolve);
-		}, options.loadDeferredImagesMaxIdleTime * 10);
-	}
-
-	async function lazyLoadEnd(observer, options, resolve) {
-		await clearAsyncTimeout("idleTimeout");
-		if (singlefile.lib.processors.hooks.content.frames) {
-			singlefile.lib.processors.hooks.content.frames.loadDeferredImagesEnd(options);
 		}
-		await setAsyncTimeout("endTimeout", async () => {
-			await clearAsyncTimeout("maxTimeout");
-			resolve();
-		}, options.loadDeferredImagesMaxIdleTime / 2);
-		observer.disconnect();
-	}
 
-	async function setAsyncTimeout(type, callback, delay) {
-		if (browser && browser.runtime && browser.runtime.sendMessage) {
-			if (!timeouts.get(type) || !timeouts.get(type).pending) {
-				const timeoutData = { callback, pending: true };
-				timeouts.set(type, timeoutData);
-				await browser.runtime.sendMessage({ method: "singlefile.lazyTimeout.setTimeout", type, delay });
-				timeoutData.pending = false;
-			}
-		} else {
-			const timeoutId = timeouts.get(type);
-			if (timeoutId) {
-				globalThis.clearTimeout(timeoutId);
-			}
-			timeouts.set(type, callback);
-			globalThis.setTimeout(callback, delay);
+		function cleanupAndResolve(value) {
+			observer.disconnect();
+			removeEventListener(hooksFrames.LOAD_IMAGE_EVENT, onImageLoadEvent);
+			removeEventListener(hooksFrames.IMAGE_LOADED_EVENT, onImageLoadedEvent);
+			resolve(value);
+		}
+	});
+}
+
+async function deferLazyLoadEnd(observer, options, resolve) {
+	await setAsyncTimeout("loadTimeout", () => lazyLoadEnd(observer, options, resolve), options.loadDeferredImagesMaxIdleTime);
+}
+
+async function deferForceLazyLoadEnd(observer, options, resolve) {
+	await setAsyncTimeout("maxTimeout", async () => {
+		await clearAsyncTimeout("loadTimeout");
+		await lazyLoadEnd(observer, options, resolve);
+	}, options.loadDeferredImagesMaxIdleTime * 10);
+}
+
+async function lazyLoadEnd(observer, options, resolve) {
+	await clearAsyncTimeout("idleTimeout");
+	hooksFrames.loadDeferredImagesEnd(options);
+	await setAsyncTimeout("endTimeout", async () => {
+		await clearAsyncTimeout("maxTimeout");
+		resolve();
+	}, options.loadDeferredImagesMaxIdleTime / 2);
+	observer.disconnect();
+}
+
+async function setAsyncTimeout(type, callback, delay) {
+	if (browser && browser.runtime && browser.runtime.sendMessage) {
+		if (!timeouts.get(type) || !timeouts.get(type).pending) {
+			const timeoutData = { callback, pending: true };
+			timeouts.set(type, timeoutData);
+			await browser.runtime.sendMessage({ method: "singlefile.lazyTimeout.setTimeout", type, delay });
+			timeoutData.pending = false;
 		}
+	} else {
+		const timeoutId = timeouts.get(type);
+		if (timeoutId) {
+			globalThis.clearTimeout(timeoutId);
+		}
+		timeouts.set(type, callback);
+		globalThis.setTimeout(callback, delay);
 	}
-
-	async function clearAsyncTimeout(type) {
-		if (browser && browser.runtime && browser.runtime.sendMessage) {
-			await browser.runtime.sendMessage({ method: "singlefile.lazyTimeout.clearTimeout", type });
-		} else {
-			const previousTimeoutId = timeouts.get(type);
-			timeouts.delete(type);
-			if (previousTimeoutId) {
-				globalThis.clearTimeout(previousTimeoutId);
-			}
+}
+
+async function clearAsyncTimeout(type) {
+	if (browser && browser.runtime && browser.runtime.sendMessage) {
+		await browser.runtime.sendMessage({ method: "singlefile.lazyTimeout.clearTimeout", type });
+	} else {
+		const previousTimeoutId = timeouts.get(type);
+		timeouts.delete(type);
+		if (previousTimeoutId) {
+			globalThis.clearTimeout(previousTimeoutId);
 		}
 	}
-
-})(typeof globalThis == "object" ? globalThis : window);
+}

+ 27 - 0
lib/single-file/rollup.config.js

@@ -0,0 +1,27 @@
+import { terser } from "rollup-plugin-terser";
+
+export default [{
+	input: ["index.js"],
+	output: [{
+		file: "dist/single-file.js",
+		format: "umd",
+		name: "singlefile",
+		plugins: [terser()]
+	}]
+},{
+	input: ["single-file-frames.js"],
+	output: [{
+		file: "dist/single-file-frames.js",
+		format: "umd",
+		name: "singlefile",
+		plugins: [terser()]
+	}]
+},{
+	input: ["single-file-bootstrap.js"],
+	output: [{
+		file: "dist/single-file-bootstrap.js",
+		format: "umd",
+		name: "singlefile",
+		plugins: [terser()]
+	}]
+}];

+ 30 - 0
lib/single-file/single-file-bootstrap.js

@@ -0,0 +1,30 @@
+import * as frameTree from "./processors/frame-tree/content/content-frame-tree.js";
+import {
+	COMMENT_HEADER,
+	COMMENT_HEADER_LEGACY,
+	ON_BEFORE_CAPTURE_EVENT_NAME,
+	ON_AFTER_CAPTURE_EVENT_NAME,
+	waitForUserScript,
+	preProcessDoc,
+	postProcessDoc,
+	serialize,
+	getShadowRoot
+} from "./single-file-helper.js";
+
+const processors = { frameTree };
+const helper = {
+	COMMENT_HEADER,
+	COMMENT_HEADER_LEGACY,
+	ON_BEFORE_CAPTURE_EVENT_NAME,
+	ON_AFTER_CAPTURE_EVENT_NAME,
+	waitForUserScript,
+	preProcessDoc,
+	postProcessDoc,
+	serialize,
+	getShadowRoot
+};
+
+export {
+	helper,
+	processors
+};

+ 1924 - 1926
lib/single-file/single-file-core.js

@@ -21,2219 +21,2217 @@
  *   Source.
  */
 
-/* global window, globalThis */
-
-this.singlefile.lib.core = this.singlefile.lib.core || (globalThis => {
-
-	const DEBUG = false;
-
-	const Set = globalThis.Set;
-	const Map = globalThis.Map;
-
-	let util, cssTree;
-
-	function getClass(...args) {
-		[util, cssTree] = args;
-		return SingleFileClass;
-	}
-
-	class SingleFileClass {
-		constructor(options) {
-			this.options = options;
-		}
-		async run() {
-			if (this.options.userScriptEnabled) {
-				await util.waitForUserScript(util.ON_BEFORE_CAPTURE_EVENT_NAME);
-			}
-			this.runner = new Runner(this.options, true);
-			await this.runner.loadPage();
-			await this.runner.initialize();
-			if (this.options.userScriptEnabled) {
-				await util.waitForUserScript(util.ON_AFTER_CAPTURE_EVENT_NAME);
-			}
-			await this.runner.run();
-		}
-		cancel() {
-			this.cancelled = true;
-			if (this.runner) {
-				this.runner.cancel();
-			}
-		}
-		getPageData() {
-			return this.runner.getPageData();
-		}
-	}
-
-	// -------------
-	// ProgressEvent
-	// -------------
-	const PAGE_LOADING = "page-loading";
-	const PAGE_LOADED = "page-loaded";
-	const RESOURCES_INITIALIZING = "resource-initializing";
-	const RESOURCES_INITIALIZED = "resources-initialized";
-	const RESOURCE_LOADED = "resource-loaded";
-	const PAGE_ENDED = "page-ended";
-	const STAGE_STARTED = "stage-started";
-	const STAGE_ENDED = "stage-ended";
-	const STAGE_TASK_STARTED = "stage-task-started";
-	const STAGE_TASK_ENDED = "stage-task-ended";
-
-	class ProgressEvent {
-		constructor(type, detail) {
-			return { type, detail, PAGE_LOADING, PAGE_LOADED, RESOURCES_INITIALIZING, RESOURCES_INITIALIZED, RESOURCE_LOADED, PAGE_ENDED, STAGE_STARTED, STAGE_ENDED, STAGE_TASK_STARTED, STAGE_TASK_ENDED };
-		}
-	}
-
-	// ------
-	// Runner
-	// ------
-	const RESOLVE_URLS_STAGE = 0;
-	const REPLACE_DATA_STAGE = 1;
-	const REPLACE_DOCS_STAGE = 2;
-	const POST_PROCESS_STAGE = 3;
-	const STAGES = [{
-		sequential: [
-			{ action: "preProcessPage" },
-			{ option: "loadDeferredImagesKeepZoomLevel", action: "resetZoomLevel" },
-			{ action: "replaceStyleContents" },
-			{ action: "resetCharsetMeta" },
-			{ option: "saveFavicon", action: "saveFavicon" },
-			{ action: "replaceCanvasElements" },
-			{ action: "insertFonts" },
-			{ action: "insertShadowRootContents" },
-			{ action: "setInputValues" },
-			{ option: "removeScripts", action: "removeScripts" },
-			{ option: "selected", action: "removeUnselectedElements" },
-			{ option: "removeVideoSrc", action: "insertVideoPosters" },
-			{ option: "removeFrames", action: "removeFrames" },
-			{ option: "removeVideoSrc", action: "removeVideoSources" },
-			{ option: "removeAudioSrc", action: "removeAudioSources" },
-			{ action: "removeDiscardedResources" },
-			{ option: "removeHiddenElements", action: "removeHiddenElements" },
-			{ action: "resolveHrefs" },
-			{ action: "resolveStyleAttributeURLs" }
-		],
-		parallel: [
-			{ action: "resolveStylesheetURLs" },
-			{ option: "!removeFrames", action: "resolveFrameURLs" },
-			{ action: "resolveHtmlImportURLs" }
-		]
-	}, {
-		sequential: [
-			{ option: "removeUnusedStyles", action: "removeUnusedStyles" },
-			{ option: "removeAlternativeMedias", action: "removeAlternativeMedias" },
-			{ option: "removeUnusedFonts", action: "removeUnusedFonts" }
-		],
-		parallel: [
-			{ action: "processStylesheets" },
-			{ action: "processStyleAttributes" },
-			{ action: "processPageResources" },
-			{ option: "!removeScripts", action: "processScripts" }
-		]
-	}, {
-		sequential: [
-			{ option: "removeAlternativeImages", action: "removeAlternativeImages" }
-		],
-		parallel: [
-			{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
-			{ option: "!removeFrames", action: "processFrames" },
-			{ option: "!removeImports", action: "processHtmlImports" },
-		]
-	}, {
-		sequential: [
-			{ action: "replaceStylesheets" },
-			{ action: "replaceStyleAttributes" },
-			{ action: "insertVariables" },
-			{ option: "compressHTML", action: "compressHTML" },
-			{ action: "cleanupPage" }
-		],
-		parallel: [
-			{ option: "enableMaff", action: "insertMAFFMetaData" },
-			{ action: "setDocInfo" }
-		]
-	}];
-
-	class Runner {
-		constructor(options, root) {
-			const rootDocDefined = root && options.doc;
-			this.root = root;
-			this.options = options;
-			this.options.url = this.options.url || (rootDocDefined && this.options.doc.location.href);
-			const matchResourceReferrer = this.options.url.match(/^.*\//);
-			this.options.resourceReferrer = this.options.passReferrerOnError && matchResourceReferrer && matchResourceReferrer[0];
-			this.options.baseURI = rootDocDefined && this.options.doc.baseURI;
-			this.options.rootDocument = root;
-			this.options.updatedResources = this.options.updatedResources || {};
-			this.options.fontTests = new Map();
-			this.batchRequest = new BatchRequest();
-			this.processor = new Processor(options, this.batchRequest);
-			if (rootDocDefined) {
-				const docData = util.preProcessDoc(this.options.doc, this.options.win, this.options);
-				this.options.canvases = docData.canvases;
-				this.options.fonts = docData.fonts;
-				this.options.stylesheets = docData.stylesheets;
-				this.options.images = docData.images;
-				this.options.posters = docData.posters;
-				this.options.usedFonts = docData.usedFonts;
-				this.options.shadowRoots = docData.shadowRoots;
-				this.options.imports = docData.imports;
-				this.options.referrer = docData.referrer;
-				this.markedElements = docData.markedElements;
-			}
-			if (this.options.saveRawPage) {
-				this.options.removeFrames = true;
-			}
-			this.options.content = this.options.content || (rootDocDefined ? util.serialize(this.options.doc) : null);
-			this.onprogress = options.onprogress || (() => { });
-		}
-
-		async loadPage() {
-			this.onprogress(new ProgressEvent(PAGE_LOADING, { pageURL: this.options.url, frame: !this.root }));
-			await this.processor.loadPage(this.options.content);
-			this.onprogress(new ProgressEvent(PAGE_LOADED, { pageURL: this.options.url, frame: !this.root }));
-		}
-
-		async initialize() {
-			this.onprogress(new ProgressEvent(RESOURCES_INITIALIZING, { pageURL: this.options.url }));
-			await this.executeStage(RESOLVE_URLS_STAGE);
-			this.pendingPromises = this.executeStage(REPLACE_DATA_STAGE);
-			if (this.root && this.options.doc) {
-				util.postProcessDoc(this.options.doc, this.markedElements);
-			}
-		}
-
-		cancel() {
-			this.cancelled = true;
-			this.batchRequest.cancel();
-			if (this.root) {
-				if (this.options.frames) {
-					this.options.frames.forEach(cancelRunner);
-				}
-				if (this.options.imports) {
-					this.options.imports.forEach(cancelRunner);
-				}
-			}
+/* global globalThis */
 
-			function cancelRunner(resourceData) {
-				if (resourceData.runner) {
-					resourceData.runner.cancel();
-				}
-			}
-		}
+const DEBUG = false;
 
-		async run() {
-			if (this.root) {
-				this.processor.initialize(this.batchRequest);
-				this.onprogress(new ProgressEvent(RESOURCES_INITIALIZED, { pageURL: this.options.url, max: this.processor.maxResources }));
-			}
-			await this.batchRequest.run(detail => {
-				detail.pageURL = this.options.url;
-				this.onprogress(new ProgressEvent(RESOURCE_LOADED, detail));
-			}, this.options);
-			await this.pendingPromises;
-			this.options.doc = null;
-			this.options.win = null;
-			await this.executeStage(REPLACE_DOCS_STAGE);
-			await this.executeStage(POST_PROCESS_STAGE);
-			this.processor.finalize();
+const Set = globalThis.Set;
+const Map = globalThis.Map;
+
+let util, cssTree;
+
+function getClass(...args) {
+	[util, cssTree] = args;
+	return SingleFileClass;
+}
+
+class SingleFileClass {
+	constructor(options) {
+		this.options = options;
+	}
+	async run() {
+		if (this.options.userScriptEnabled && util.waitForUserScript.callback) {
+			await util.waitForUserScript.callback(util.ON_BEFORE_CAPTURE_EVENT_NAME);
+		}
+		this.runner = new Runner(this.options, true);
+		await this.runner.loadPage();
+		await this.runner.initialize();
+		if (this.options.userScriptEnabled && util.waitForUserScript.callback) {
+			await util.waitForUserScript.callback(util.ON_AFTER_CAPTURE_EVENT_NAME);
+		}
+		await this.runner.run();
+	}
+	cancel() {
+		this.cancelled = true;
+		if (this.runner) {
+			this.runner.cancel();
 		}
+	}
+	getPageData() {
+		return this.runner.getPageData();
+	}
+}
+
+// -------------
+// ProgressEvent
+// -------------
+const PAGE_LOADING = "page-loading";
+const PAGE_LOADED = "page-loaded";
+const RESOURCES_INITIALIZING = "resource-initializing";
+const RESOURCES_INITIALIZED = "resources-initialized";
+const RESOURCE_LOADED = "resource-loaded";
+const PAGE_ENDED = "page-ended";
+const STAGE_STARTED = "stage-started";
+const STAGE_ENDED = "stage-ended";
+const STAGE_TASK_STARTED = "stage-task-started";
+const STAGE_TASK_ENDED = "stage-task-ended";
+
+class ProgressEvent {
+	constructor(type, detail) {
+		return { type, detail, PAGE_LOADING, PAGE_LOADED, RESOURCES_INITIALIZING, RESOURCES_INITIALIZED, RESOURCE_LOADED, PAGE_ENDED, STAGE_STARTED, STAGE_ENDED, STAGE_TASK_STARTED, STAGE_TASK_ENDED };
+	}
+}
+
+// ------
+// Runner
+// ------
+const RESOLVE_URLS_STAGE = 0;
+const REPLACE_DATA_STAGE = 1;
+const REPLACE_DOCS_STAGE = 2;
+const POST_PROCESS_STAGE = 3;
+const STAGES = [{
+	sequential: [
+		{ action: "preProcessPage" },
+		{ option: "loadDeferredImagesKeepZoomLevel", action: "resetZoomLevel" },
+		{ action: "replaceStyleContents" },
+		{ action: "resetCharsetMeta" },
+		{ option: "saveFavicon", action: "saveFavicon" },
+		{ action: "replaceCanvasElements" },
+		{ action: "insertFonts" },
+		{ action: "insertShadowRootContents" },
+		{ action: "setInputValues" },
+		{ option: "removeScripts", action: "removeScripts" },
+		{ option: "selected", action: "removeUnselectedElements" },
+		{ option: "removeVideoSrc", action: "insertVideoPosters" },
+		{ option: "removeFrames", action: "removeFrames" },
+		{ option: "removeVideoSrc", action: "removeVideoSources" },
+		{ option: "removeAudioSrc", action: "removeAudioSources" },
+		{ action: "removeDiscardedResources" },
+		{ option: "removeHiddenElements", action: "removeHiddenElements" },
+		{ action: "resolveHrefs" },
+		{ action: "resolveStyleAttributeURLs" }
+	],
+	parallel: [
+		{ action: "resolveStylesheetURLs" },
+		{ option: "!removeFrames", action: "resolveFrameURLs" },
+		{ action: "resolveHtmlImportURLs" }
+	]
+}, {
+	sequential: [
+		{ option: "removeUnusedStyles", action: "removeUnusedStyles" },
+		{ option: "removeAlternativeMedias", action: "removeAlternativeMedias" },
+		{ option: "removeUnusedFonts", action: "removeUnusedFonts" }
+	],
+	parallel: [
+		{ action: "processStylesheets" },
+		{ action: "processStyleAttributes" },
+		{ action: "processPageResources" },
+		{ option: "!removeScripts", action: "processScripts" }
+	]
+}, {
+	sequential: [
+		{ option: "removeAlternativeImages", action: "removeAlternativeImages" }
+	],
+	parallel: [
+		{ option: "removeAlternativeFonts", action: "removeAlternativeFonts" },
+		{ option: "!removeFrames", action: "processFrames" },
+		{ option: "!removeImports", action: "processHtmlImports" },
+	]
+}, {
+	sequential: [
+		{ action: "replaceStylesheets" },
+		{ action: "replaceStyleAttributes" },
+		{ action: "insertVariables" },
+		{ option: "compressHTML", action: "compressHTML" },
+		{ action: "cleanupPage" }
+	],
+	parallel: [
+		{ option: "enableMaff", action: "insertMAFFMetaData" },
+		{ action: "setDocInfo" }
+	]
+}];
+
+class Runner {
+	constructor(options, root) {
+		const rootDocDefined = root && options.doc;
+		this.root = root;
+		this.options = options;
+		this.options.url = this.options.url || (rootDocDefined && this.options.doc.location.href);
+		const matchResourceReferrer = this.options.url.match(/^.*\//);
+		this.options.resourceReferrer = this.options.passReferrerOnError && matchResourceReferrer && matchResourceReferrer[0];
+		this.options.baseURI = rootDocDefined && this.options.doc.baseURI;
+		this.options.rootDocument = root;
+		this.options.updatedResources = this.options.updatedResources || {};
+		this.options.fontTests = new Map();
+		this.batchRequest = new BatchRequest();
+		this.processor = new Processor(options, this.batchRequest);
+		if (rootDocDefined) {
+			const docData = util.preProcessDoc(this.options.doc, this.options.win, this.options);
+			this.options.canvases = docData.canvases;
+			this.options.fonts = docData.fonts;
+			this.options.stylesheets = docData.stylesheets;
+			this.options.images = docData.images;
+			this.options.posters = docData.posters;
+			this.options.usedFonts = docData.usedFonts;
+			this.options.shadowRoots = docData.shadowRoots;
+			this.options.imports = docData.imports;
+			this.options.referrer = docData.referrer;
+			this.markedElements = docData.markedElements;
+		}
+		if (this.options.saveRawPage) {
+			this.options.removeFrames = true;
+		}
+		this.options.content = this.options.content || (rootDocDefined ? util.serialize(this.options.doc) : null);
+		this.onprogress = options.onprogress || (() => { });
+	}
+
+	async loadPage() {
+		this.onprogress(new ProgressEvent(PAGE_LOADING, { pageURL: this.options.url, frame: !this.root }));
+		await this.processor.loadPage(this.options.content);
+		this.onprogress(new ProgressEvent(PAGE_LOADED, { pageURL: this.options.url, frame: !this.root }));
+	}
 
-		getDocument() {
-			return this.processor.doc;
+	async initialize() {
+		this.onprogress(new ProgressEvent(RESOURCES_INITIALIZING, { pageURL: this.options.url }));
+		await this.executeStage(RESOLVE_URLS_STAGE);
+		this.pendingPromises = this.executeStage(REPLACE_DATA_STAGE);
+		if (this.root && this.options.doc) {
+			util.postProcessDoc(this.options.doc, this.markedElements);
 		}
+	}
 
-		getStyleSheets() {
-			return this.processor.stylesheets;
+	cancel() {
+		this.cancelled = true;
+		this.batchRequest.cancel();
+		if (this.root) {
+			if (this.options.frames) {
+				this.options.frames.forEach(cancelRunner);
+			}
+			if (this.options.imports) {
+				this.options.imports.forEach(cancelRunner);
+			}
 		}
 
-		getPageData() {
-			if (this.root) {
-				this.onprogress(new ProgressEvent(PAGE_ENDED, { pageURL: this.options.url }));
+		function cancelRunner(resourceData) {
+			if (resourceData.runner) {
+				resourceData.runner.cancel();
 			}
-			return this.processor.getPageData();
 		}
+	}
 
-		async executeStage(step) {
+	async run() {
+		if (this.root) {
+			this.processor.initialize(this.batchRequest);
+			this.onprogress(new ProgressEvent(RESOURCES_INITIALIZED, { pageURL: this.options.url, max: this.processor.maxResources }));
+		}
+		await this.batchRequest.run(detail => {
+			detail.pageURL = this.options.url;
+			this.onprogress(new ProgressEvent(RESOURCE_LOADED, detail));
+		}, this.options);
+		await this.pendingPromises;
+		this.options.doc = null;
+		this.options.win = null;
+		await this.executeStage(REPLACE_DOCS_STAGE);
+		await this.executeStage(POST_PROCESS_STAGE);
+		this.processor.finalize();
+	}
+
+	getDocument() {
+		return this.processor.doc;
+	}
+
+	getStyleSheets() {
+		return this.processor.stylesheets;
+	}
+
+	getPageData() {
+		if (this.root) {
+			this.onprogress(new ProgressEvent(PAGE_ENDED, { pageURL: this.options.url }));
+		}
+		return this.processor.getPageData();
+	}
+
+	async executeStage(step) {
+		if (DEBUG) {
+			log("**** STARTED STAGE", step, "****");
+		}
+		const frame = !this.root;
+		this.onprogress(new ProgressEvent(STAGE_STARTED, { pageURL: this.options.url, step, frame }));
+		STAGES[step].sequential.forEach(task => {
+			let startTime;
+			if (DEBUG) {
+				startTime = Date.now();
+				log("  -- STARTED task =", task.action);
+			}
+			this.onprogress(new ProgressEvent(STAGE_TASK_STARTED, { pageURL: this.options.url, step, task: task.action, frame }));
+			if (!this.cancelled) {
+				this.executeTask(task);
+			}
+			this.onprogress(new ProgressEvent(STAGE_TASK_ENDED, { pageURL: this.options.url, step, task: task.action, frame }));
 			if (DEBUG) {
-				log("**** STARTED STAGE", step, "****");
+				log("  -- ENDED   task =", task.action, "delay =", Date.now() - startTime);
 			}
-			const frame = !this.root;
-			this.onprogress(new ProgressEvent(STAGE_STARTED, { pageURL: this.options.url, step, frame }));
-			STAGES[step].sequential.forEach(task => {
+		});
+		let parallelTasksPromise;
+		if (STAGES[step].parallel) {
+			parallelTasksPromise = await Promise.all(STAGES[step].parallel.map(async task => {
 				let startTime;
 				if (DEBUG) {
 					startTime = Date.now();
-					log("  -- STARTED task =", task.action);
+					log("  // STARTED task =", task.action);
 				}
 				this.onprogress(new ProgressEvent(STAGE_TASK_STARTED, { pageURL: this.options.url, step, task: task.action, frame }));
 				if (!this.cancelled) {
-					this.executeTask(task);
+					await this.executeTask(task);
 				}
 				this.onprogress(new ProgressEvent(STAGE_TASK_ENDED, { pageURL: this.options.url, step, task: task.action, frame }));
 				if (DEBUG) {
-					log("  -- ENDED   task =", task.action, "delay =", Date.now() - startTime);
+					log("  // ENDED task =", task.action, "delay =", Date.now() - startTime);
 				}
-			});
-			let parallelTasksPromise;
-			if (STAGES[step].parallel) {
-				parallelTasksPromise = await Promise.all(STAGES[step].parallel.map(async task => {
-					let startTime;
-					if (DEBUG) {
-						startTime = Date.now();
-						log("  // STARTED task =", task.action);
-					}
-					this.onprogress(new ProgressEvent(STAGE_TASK_STARTED, { pageURL: this.options.url, step, task: task.action, frame }));
-					if (!this.cancelled) {
-						await this.executeTask(task);
-					}
-					this.onprogress(new ProgressEvent(STAGE_TASK_ENDED, { pageURL: this.options.url, step, task: task.action, frame }));
-					if (DEBUG) {
-						log("  // ENDED task =", task.action, "delay =", Date.now() - startTime);
-					}
-				}));
-			} else {
-				parallelTasksPromise = Promise.resolve();
-			}
-			this.onprogress(new ProgressEvent(STAGE_ENDED, { pageURL: this.options.url, step, frame }));
-			if (DEBUG) {
-				log("**** ENDED   STAGE", step, "****");
-			}
-			return parallelTasksPromise;
+			}));
+		} else {
+			parallelTasksPromise = Promise.resolve();
 		}
-
-		executeTask(task) {
-			if (!task.option || ((task.option.startsWith("!") && !this.options[task.option]) || this.options[task.option])) {
-				return this.processor[task.action]();
-			}
+		this.onprogress(new ProgressEvent(STAGE_ENDED, { pageURL: this.options.url, step, frame }));
+		if (DEBUG) {
+			log("**** ENDED   STAGE", step, "****");
 		}
+		return parallelTasksPromise;
 	}
 
-	// ------------
-	// BatchRequest
-	// ------------
-	class BatchRequest {
-		constructor() {
-			this.requests = new Map();
-			this.duplicates = new Map();
+	executeTask(task) {
+		if (!task.option || ((task.option.startsWith("!") && !this.options[task.option]) || this.options[task.option])) {
+			return this.processor[task.action]();
 		}
+	}
+}
+
+// ------------
+// BatchRequest
+// ------------
+class BatchRequest {
+	constructor() {
+		this.requests = new Map();
+		this.duplicates = new Map();
+	}
 
-		addURL(resourceURL, asBinary, expectedType, groupDuplicates) {
-			return new Promise((resolve, reject) => {
-				const requestKey = JSON.stringify([resourceURL, asBinary, expectedType]);
-				let resourceRequests = this.requests.get(requestKey);
-				if (!resourceRequests) {
-					resourceRequests = [];
-					this.requests.set(requestKey, resourceRequests);
-				}
-				const callbacks = { resolve, reject };
-				resourceRequests.push(callbacks);
-				if (groupDuplicates) {
-					let duplicateRequests = this.duplicates.get(requestKey);
-					if (!duplicateRequests) {
-						duplicateRequests = [];
-						this.duplicates.set(requestKey, duplicateRequests);
-					}
-					duplicateRequests.push(callbacks);
-				}
-			});
-		}
+	addURL(resourceURL, asBinary, expectedType, groupDuplicates) {
+		return new Promise((resolve, reject) => {
+			const requestKey = JSON.stringify([resourceURL, asBinary, expectedType]);
+			let resourceRequests = this.requests.get(requestKey);
+			if (!resourceRequests) {
+				resourceRequests = [];
+				this.requests.set(requestKey, resourceRequests);
+			}
+			const callbacks = { resolve, reject };
+			resourceRequests.push(callbacks);
+			if (groupDuplicates) {
+				let duplicateRequests = this.duplicates.get(requestKey);
+				if (!duplicateRequests) {
+					duplicateRequests = [];
+					this.duplicates.set(requestKey, duplicateRequests);
+				}
+				duplicateRequests.push(callbacks);
+			}
+		});
+	}
 
-		getMaxResources() {
-			return this.requests.size;
-		}
+	getMaxResources() {
+		return this.requests.size;
+	}
 
-		run(onloadListener, options) {
-			const resourceURLs = [...this.requests.keys()];
-			let indexResource = 0;
-			return Promise.all(resourceURLs.map(async requestKey => {
-				const [resourceURL, asBinary, expectedType] = JSON.parse(requestKey);
-				const resourceRequests = this.requests.get(requestKey);
-				try {
-					const currentIndexResource = indexResource;
-					indexResource = indexResource + 1;
-					const content = await util.getContent(resourceURL, {
-						asBinary,
-						expectedType,
-						maxResourceSize: options.maxResourceSize,
-						maxResourceSizeEnabled: options.maxResourceSizeEnabled,
-						frameId: options.windowId,
-						resourceReferrer: options.resourceReferrer
+	run(onloadListener, options) {
+		const resourceURLs = [...this.requests.keys()];
+		let indexResource = 0;
+		return Promise.all(resourceURLs.map(async requestKey => {
+			const [resourceURL, asBinary, expectedType] = JSON.parse(requestKey);
+			const resourceRequests = this.requests.get(requestKey);
+			try {
+				const currentIndexResource = indexResource;
+				indexResource = indexResource + 1;
+				const content = await util.getContent(resourceURL, {
+					asBinary,
+					expectedType,
+					maxResourceSize: options.maxResourceSize,
+					maxResourceSizeEnabled: options.maxResourceSizeEnabled,
+					frameId: options.windowId,
+					resourceReferrer: options.resourceReferrer
+				});
+				onloadListener({ url: resourceURL });
+				if (!this.cancelled) {
+					resourceRequests.forEach(callbacks => {
+						const duplicateCallbacks = this.duplicates.get(requestKey);
+						const duplicate = duplicateCallbacks && duplicateCallbacks.length > 1 && duplicateCallbacks.includes(callbacks);
+						callbacks.resolve({ content: content.data, indexResource: currentIndexResource, duplicate });
 					});
-					onloadListener({ url: resourceURL });
-					if (!this.cancelled) {
-						resourceRequests.forEach(callbacks => {
-							const duplicateCallbacks = this.duplicates.get(requestKey);
-							const duplicate = duplicateCallbacks && duplicateCallbacks.length > 1 && duplicateCallbacks.includes(callbacks);
-							callbacks.resolve({ content: content.data, indexResource: currentIndexResource, duplicate });
-						});
-					}
-				} catch (error) {
-					indexResource = indexResource + 1;
-					onloadListener({ url: resourceURL });
-					resourceRequests.forEach(resourceRequest => resourceRequest.reject(error));
 				}
-				this.requests.delete(requestKey);
-			}));
-		}
-
-		cancel() {
-			this.cancelled = true;
-			const resourceURLs = [...this.requests.keys()];
-			resourceURLs.forEach(requestKey => {
-				const resourceRequests = this.requests.get(requestKey);
-				resourceRequests.forEach(callbacks => callbacks.reject());
-				this.requests.delete(requestKey);
-			});
-		}
+			} catch (error) {
+				indexResource = indexResource + 1;
+				onloadListener({ url: resourceURL });
+				resourceRequests.forEach(resourceRequest => resourceRequest.reject(error));
+			}
+			this.requests.delete(requestKey);
+		}));
 	}
 
-	// ---------
-	// Processor
-	// ---------
-	const PREFIXES_FORBIDDEN_DATA_URI = ["data:text/"];
-	const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
-	const EMPTY_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
-	const SCRIPT_TAG_FOUND = /<script/gi;
-	const NOSCRIPT_TAG_FOUND = /<noscript/gi;
-	const SHADOW_MODE_ATTRIBUTE_NAME = "shadowmode";
-	const SHADOW_DELEGATE_FOCUS_ATTRIBUTE_NAME = "delegatesfocus";
-	const SCRIPT_TEMPLATE_SHADOW_ROOT = "data-template-shadow-root";
-	const UTF8_CHARSET = "utf-8";
+	cancel() {
+		this.cancelled = true;
+		const resourceURLs = [...this.requests.keys()];
+		resourceURLs.forEach(requestKey => {
+			const resourceRequests = this.requests.get(requestKey);
+			resourceRequests.forEach(callbacks => callbacks.reject());
+			this.requests.delete(requestKey);
+		});
+	}
+}
+
+// ---------
+// Processor
+// ---------
+const PREFIXES_FORBIDDEN_DATA_URI = ["data:text/"];
+const PREFIX_DATA_URI_IMAGE_SVG = "data:image/svg+xml";
+const EMPTY_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
+const SCRIPT_TAG_FOUND = /<script/gi;
+const NOSCRIPT_TAG_FOUND = /<noscript/gi;
+const SHADOW_MODE_ATTRIBUTE_NAME = "shadowmode";
+const SHADOW_DELEGATE_FOCUS_ATTRIBUTE_NAME = "delegatesfocus";
+const SCRIPT_TEMPLATE_SHADOW_ROOT = "data-template-shadow-root";
+const UTF8_CHARSET = "utf-8";
+
+class Processor {
+	constructor(options, batchRequest) {
+		this.options = options;
+		this.stats = new Stats(options);
+		this.baseURI = normalizeURL(options.baseURI || options.url);
+		this.batchRequest = batchRequest;
+		this.stylesheets = new Map();
+		this.styles = new Map();
+		this.cssVariables = new Map();
+		this.fontTests = options.fontTests;
+	}
 
-	class Processor {
-		constructor(options, batchRequest) {
-			this.options = options;
-			this.stats = new Stats(options);
-			this.baseURI = normalizeURL(options.baseURI || options.url);
-			this.batchRequest = batchRequest;
-			this.stylesheets = new Map();
-			this.styles = new Map();
-			this.cssVariables = new Map();
-			this.fontTests = options.fontTests;
+	initialize() {
+		this.options.saveDate = new Date();
+		this.options.saveUrl = this.options.url;
+		if (this.options.enableMaff) {
+			this.maffMetaDataPromise = this.batchRequest.addURL(util.resolveURL("index.rdf", this.options.baseURI || this.options.url));
 		}
-
-		initialize() {
-			this.options.saveDate = new Date();
-			this.options.saveUrl = this.options.url;
-			if (this.options.enableMaff) {
-				this.maffMetaDataPromise = this.batchRequest.addURL(util.resolveURL("index.rdf", this.options.baseURI || this.options.url));
-			}
-			this.maxResources = this.batchRequest.getMaxResources();
-			if (!this.options.saveRawPage && !this.options.removeFrames && this.options.frames) {
-				this.options.frames.forEach(frameData => this.maxResources += frameData.maxResources || 0);
-			}
-			if (!this.options.removeImports && this.options.imports) {
-				this.options.imports.forEach(importData => this.maxResources += importData.maxResources || 0);
-			}
-			this.stats.set("processed", "resources", this.maxResources);
+		this.maxResources = this.batchRequest.getMaxResources();
+		if (!this.options.saveRawPage && !this.options.removeFrames && this.options.frames) {
+			this.options.frames.forEach(frameData => this.maxResources += frameData.maxResources || 0);
+		}
+		if (!this.options.removeImports && this.options.imports) {
+			this.options.imports.forEach(importData => this.maxResources += importData.maxResources || 0);
 		}
+		this.stats.set("processed", "resources", this.maxResources);
+	}
 
-		async loadPage(pageContent, charset) {
-			let content;
-			if (!pageContent || this.options.saveRawPage) {
-				content = await util.getContent(this.baseURI, {
-					maxResourceSize: this.options.maxResourceSize,
-					maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
-					charset,
-					frameId: this.options.windowId,
-					resourceReferrer: this.options.resourceReferrer
-				});
-				pageContent = content.data;
-			}
-			this.doc = util.parseDocContent(pageContent, this.baseURI);
-			if (this.options.saveRawPage) {
-				let charset;
-				this.doc.querySelectorAll("meta[charset], meta[http-equiv=\"content-type\"]").forEach(element => {
-					const charsetDeclaration = element.content.split(";")[1];
-					if (charsetDeclaration && !charset) {
-						charset = charsetDeclaration.split("=")[1].trim().toLowerCase();
-					}
-				});
-				if (charset && charset != content.charset) {
-					return this.loadPage(pageContent, charset);
+	async loadPage(pageContent, charset) {
+		let content;
+		if (!pageContent || this.options.saveRawPage) {
+			content = await util.getContent(this.baseURI, {
+				maxResourceSize: this.options.maxResourceSize,
+				maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
+				charset,
+				frameId: this.options.windowId,
+				resourceReferrer: this.options.resourceReferrer
+			});
+			pageContent = content.data;
+		}
+		this.doc = util.parseDocContent(pageContent, this.baseURI);
+		if (this.options.saveRawPage) {
+			let charset;
+			this.doc.querySelectorAll("meta[charset], meta[http-equiv=\"content-type\"]").forEach(element => {
+				const charsetDeclaration = element.content.split(";")[1];
+				if (charsetDeclaration && !charset) {
+					charset = charsetDeclaration.split("=")[1].trim().toLowerCase();
 				}
+			});
+			if (charset && charset != content.charset) {
+				return this.loadPage(pageContent, charset);
 			}
-			this.workStyleElement = this.doc.createElement("style");
-			this.doc.body.appendChild(this.workStyleElement);
-			this.onEventAttributeNames = getOnEventAttributeNames(this.doc);
 		}
+		this.workStyleElement = this.doc.createElement("style");
+		this.doc.body.appendChild(this.workStyleElement);
+		this.onEventAttributeNames = getOnEventAttributeNames(this.doc);
+	}
 
-		finalize() {
-			if (this.workStyleElement.parentNode) {
-				this.workStyleElement.remove();
-			}
+	finalize() {
+		if (this.workStyleElement.parentNode) {
+			this.workStyleElement.remove();
 		}
+	}
 
-		async getPageData() {
-			util.postProcessDoc(this.doc);
-			const url = util.parseURL(this.baseURI);
-			if (this.options.insertSingleFileComment) {
-				const firstComment = this.doc.documentElement.firstChild;
-				let infobarURL = this.options.saveUrl, infobarSaveDate = this.options.saveDate;
-				if (firstComment.nodeType == 8 && (firstComment.textContent.includes(util.COMMENT_HEADER_LEGACY) || firstComment.textContent.includes(util.COMMENT_HEADER))) {
-					const info = this.doc.documentElement.firstChild.textContent.split("\n");
-					const [, , url, saveDate] = info;
-					infobarURL = url.split("url: ")[1];
-					infobarSaveDate = saveDate.split("saved date: ")[1];
-					firstComment.remove();
-				}
-				const infobarContent = (this.options.infobarContent || "").replace(/\\n/g, "\n").replace(/\\t/g, "\t");
-				const commentNode = this.doc.createComment("\n " + (this.options.useLegacyCommentHeader ? util.COMMENT_HEADER_LEGACY : util.COMMENT_HEADER) +
-					" \n url: " + infobarURL +
-					" \n saved date: " + infobarSaveDate +
-					(infobarContent ? " \n info: " + infobarContent : "") + "\n");
-				this.doc.documentElement.insertBefore(commentNode, this.doc.documentElement.firstChild);
-			}
-			if (this.options.insertCanonicalLink && this.options.saveUrl.match(HTTP_URI_PREFIX)) {
-				let canonicalLink = this.doc.querySelector("link[rel=canonical]");
-				if (!canonicalLink) {
-					canonicalLink = this.doc.createElement("link");
-					canonicalLink.setAttribute("rel", "canonical");
-					this.doc.head.appendChild(canonicalLink);
-				}
-				if (canonicalLink && !canonicalLink.href) {
-					canonicalLink.href = this.options.saveUrl;
-				}
-			}
-			if (this.options.insertMetaNoIndex) {
-				let metaElement = this.doc.querySelector("meta[name=robots][content*=noindex]");
-				if (!metaElement) {
-					metaElement = this.doc.createElement("meta");
-					metaElement.setAttribute("name", "robots");
-					metaElement.setAttribute("content", "noindex");
-					this.doc.head.appendChild(metaElement);
-				}
+	async getPageData() {
+		util.postProcessDoc(this.doc);
+		const url = util.parseURL(this.baseURI);
+		if (this.options.insertSingleFileComment) {
+			const firstComment = this.doc.documentElement.firstChild;
+			let infobarURL = this.options.saveUrl, infobarSaveDate = this.options.saveDate;
+			if (firstComment.nodeType == 8 && (firstComment.textContent.includes(util.COMMENT_HEADER_LEGACY) || firstComment.textContent.includes(util.COMMENT_HEADER))) {
+				const info = this.doc.documentElement.firstChild.textContent.split("\n");
+				const [, , url, saveDate] = info;
+				infobarURL = url.split("url: ")[1];
+				infobarSaveDate = saveDate.split("saved date: ")[1];
+				firstComment.remove();
+			}
+			const infobarContent = (this.options.infobarContent || "").replace(/\\n/g, "\n").replace(/\\t/g, "\t");
+			const commentNode = this.doc.createComment("\n " + (this.options.useLegacyCommentHeader ? util.COMMENT_HEADER_LEGACY : util.COMMENT_HEADER) +
+				" \n url: " + infobarURL +
+				" \n saved date: " + infobarSaveDate +
+				(infobarContent ? " \n info: " + infobarContent : "") + "\n");
+			this.doc.documentElement.insertBefore(commentNode, this.doc.documentElement.firstChild);
+		}
+		if (this.options.insertCanonicalLink && this.options.saveUrl.match(HTTP_URI_PREFIX)) {
+			let canonicalLink = this.doc.querySelector("link[rel=canonical]");
+			if (!canonicalLink) {
+				canonicalLink = this.doc.createElement("link");
+				canonicalLink.setAttribute("rel", "canonical");
+				this.doc.head.appendChild(canonicalLink);
+			}
+			if (canonicalLink && !canonicalLink.href) {
+				canonicalLink.href = this.options.saveUrl;
+			}
+		}
+		if (this.options.insertMetaNoIndex) {
+			let metaElement = this.doc.querySelector("meta[name=robots][content*=noindex]");
+			if (!metaElement) {
+				metaElement = this.doc.createElement("meta");
+				metaElement.setAttribute("name", "robots");
+				metaElement.setAttribute("content", "noindex");
+				this.doc.head.appendChild(metaElement);
 			}
-			let size;
-			if (this.options.displayStats) {
-				size = util.getContentSize(this.doc.documentElement.outerHTML);
-			}
-			const content = util.serialize(this.doc, this.options.compressHTML);
-			if (this.options.displayStats) {
-				const contentSize = util.getContentSize(content);
-				this.stats.set("processed", "HTML bytes", contentSize);
-				this.stats.add("discarded", "HTML bytes", size - contentSize);
-			}
-			let filename = await ProcessorHelper.evalTemplate(this.options.filenameTemplate, this.options, content) || "";
-			const replacementCharacter = this.options.filenameReplacementCharacter;
-			filename = util.getValidFilename(filename, this.options.filenameReplacedCharacters, replacementCharacter);
-			if (!this.options.backgroundSave) {
-				filename = filename.replace(/\//g, replacementCharacter);
-			}
-			if (!this.options.saveToGDrive && util.getContentSize(filename) > this.options.filenameMaxLength) {
-				const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
-				const extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
-				filename = await util.truncateText(filename, this.options.filenameMaxLength - extension.length);
-				filename = filename + "…" + extension;
-			}
-			if (!filename) {
-				filename = "Unnamed page";
-			}
-			const matchTitle = this.baseURI.match(/([^/]*)\/?(\.html?.*)$/) || this.baseURI.match(/\/\/([^/]*)\/?$/);
-			const pageData = {
-				stats: this.stats.data,
-				title: this.options.title || (this.baseURI && matchTitle ? matchTitle[1] : (url.hostname ? url.hostname : "")),
-				filename,
-				content
-			};
-			if (this.options.addProof) {
-				pageData.hash = await util.digest("SHA-256", content);
-			}
-			if (this.options.retrieveLinks) {
-				pageData.links = Array.from(new Set(Array.from(this.doc.links).map(linkElement => linkElement.href)));
-			}
-			return pageData;
-		}
-
-		preProcessPage() {
-			if (this.options.win) {
-				this.doc.body.querySelectorAll(":not(svg) title, meta, link[href][rel*=\"icon\"]").forEach(element => element instanceof this.options.win.HTMLElement && this.doc.head.appendChild(element));
-			}
-			if (this.options.images && !this.options.saveRawPage) {
-				this.doc.querySelectorAll("img[" + util.IMAGE_ATTRIBUTE_NAME + "]").forEach(imgElement => {
-					const attributeValue = imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME);
-					if (attributeValue) {
-						const imageData = this.options.images[Number(attributeValue)];
-						if (imageData) {
-							if (this.options.removeHiddenElements && (
-								(imageData.size && !imageData.size.pxWidth && !imageData.size.pxHeight) ||
-								(imgElement.getAttribute(util.HIDDEN_CONTENT_ATTRIBUTE_NAME) == "")
-							)) {
-								imgElement.setAttribute("src", EMPTY_IMAGE);
-							} else {
-								if (imageData.currentSrc) {
-									imgElement.dataset.singleFileOriginURL = imgElement.getAttribute("src");
-									imgElement.setAttribute("src", imageData.currentSrc);
-								}
-								if (this.options.loadDeferredImages) {
-									if ((!imgElement.getAttribute("src") || imgElement.getAttribute("src") == EMPTY_IMAGE) && imgElement.getAttribute("data-src")) {
-										imageData.src = imgElement.dataset.src;
-										imgElement.setAttribute("src", imgElement.dataset.src);
-										imgElement.removeAttribute("data-src");
-									}
+		}
+		let size;
+		if (this.options.displayStats) {
+			size = util.getContentSize(this.doc.documentElement.outerHTML);
+		}
+		const content = util.serialize(this.doc, this.options.compressHTML);
+		if (this.options.displayStats) {
+			const contentSize = util.getContentSize(content);
+			this.stats.set("processed", "HTML bytes", contentSize);
+			this.stats.add("discarded", "HTML bytes", size - contentSize);
+		}
+		let filename = await ProcessorHelper.evalTemplate(this.options.filenameTemplate, this.options, content) || "";
+		const replacementCharacter = this.options.filenameReplacementCharacter;
+		filename = util.getValidFilename(filename, this.options.filenameReplacedCharacters, replacementCharacter);
+		if (!this.options.backgroundSave) {
+			filename = filename.replace(/\//g, replacementCharacter);
+		}
+		if (!this.options.saveToGDrive && util.getContentSize(filename) > this.options.filenameMaxLength) {
+			const extensionMatch = filename.match(/(\.[^.]{3,4})$/);
+			const extension = extensionMatch && extensionMatch[0] && extensionMatch[0].length > 1 ? extensionMatch[0] : "";
+			filename = await util.truncateText(filename, this.options.filenameMaxLength - extension.length);
+			filename = filename + "…" + extension;
+		}
+		if (!filename) {
+			filename = "Unnamed page";
+		}
+		const matchTitle = this.baseURI.match(/([^/]*)\/?(\.html?.*)$/) || this.baseURI.match(/\/\/([^/]*)\/?$/);
+		const pageData = {
+			stats: this.stats.data,
+			title: this.options.title || (this.baseURI && matchTitle ? matchTitle[1] : (url.hostname ? url.hostname : "")),
+			filename,
+			content
+		};
+		if (this.options.addProof) {
+			pageData.hash = await util.digest("SHA-256", content);
+		}
+		if (this.options.retrieveLinks) {
+			pageData.links = Array.from(new Set(Array.from(this.doc.links).map(linkElement => linkElement.href)));
+		}
+		return pageData;
+	}
+
+	preProcessPage() {
+		if (this.options.win) {
+			this.doc.body.querySelectorAll(":not(svg) title, meta, link[href][rel*=\"icon\"]").forEach(element => element instanceof this.options.win.HTMLElement && this.doc.head.appendChild(element));
+		}
+		if (this.options.images && !this.options.saveRawPage) {
+			this.doc.querySelectorAll("img[" + util.IMAGE_ATTRIBUTE_NAME + "]").forEach(imgElement => {
+				const attributeValue = imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME);
+				if (attributeValue) {
+					const imageData = this.options.images[Number(attributeValue)];
+					if (imageData) {
+						if (this.options.removeHiddenElements && (
+							(imageData.size && !imageData.size.pxWidth && !imageData.size.pxHeight) ||
+							(imgElement.getAttribute(util.HIDDEN_CONTENT_ATTRIBUTE_NAME) == "")
+						)) {
+							imgElement.setAttribute("src", EMPTY_IMAGE);
+						} else {
+							if (imageData.currentSrc) {
+								imgElement.dataset.singleFileOriginURL = imgElement.getAttribute("src");
+								imgElement.setAttribute("src", imageData.currentSrc);
+							}
+							if (this.options.loadDeferredImages) {
+								if ((!imgElement.getAttribute("src") || imgElement.getAttribute("src") == EMPTY_IMAGE) && imgElement.getAttribute("data-src")) {
+									imageData.src = imgElement.dataset.src;
+									imgElement.setAttribute("src", imgElement.dataset.src);
+									imgElement.removeAttribute("data-src");
 								}
 							}
 						}
 					}
-				});
-				if (this.options.loadDeferredImages) {
-					this.doc.querySelectorAll("img[data-srcset]").forEach(imgElement => {
-						if (!imgElement.getAttribute("srcset") && imgElement.getAttribute("data-srcset")) {
-							imgElement.setAttribute("srcset", imgElement.dataset.srcset);
-							imgElement.removeAttribute("data-srcset");
-						}
-					});
 				}
-			}
-		}
-
-		replaceStyleContents() {
-			if (this.options.stylesheets) {
-				this.doc.querySelectorAll("style").forEach((styleElement, styleIndex) => {
-					const attributeValue = styleElement.getAttribute(util.STYLESHEET_ATTRIBUTE_NAME);
-					if (attributeValue) {
-						const stylesheetContent = this.options.stylesheets[Number(styleIndex)];
-						if (stylesheetContent) {
-							styleElement.textContent = stylesheetContent;
-						}
+			});
+			if (this.options.loadDeferredImages) {
+				this.doc.querySelectorAll("img[data-srcset]").forEach(imgElement => {
+					if (!imgElement.getAttribute("srcset") && imgElement.getAttribute("data-srcset")) {
+						imgElement.setAttribute("srcset", imgElement.dataset.srcset);
+						imgElement.removeAttribute("data-srcset");
 					}
 				});
 			}
 		}
+	}
 
-		removeUnselectedElements() {
-			removeUnmarkedElements(this.doc.body);
-			this.doc.body.removeAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME);
-
-			function removeUnmarkedElements(element) {
-				let selectedElementFound = false;
-				Array.from(element.childNodes).forEach(node => {
-					if (node.nodeType == 1) {
-						const isSelectedElement = node.getAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME) == "";
-						selectedElementFound = selectedElementFound || isSelectedElement;
-						if (isSelectedElement) {
-							node.removeAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME);
-							removeUnmarkedElements(node);
-						} else if (selectedElementFound) {
-							removeNode(node);
-						} else {
-							hideNode(node);
-						}
+	replaceStyleContents() {
+		if (this.options.stylesheets) {
+			this.doc.querySelectorAll("style").forEach((styleElement, styleIndex) => {
+				const attributeValue = styleElement.getAttribute(util.STYLESHEET_ATTRIBUTE_NAME);
+				if (attributeValue) {
+					const stylesheetContent = this.options.stylesheets[Number(styleIndex)];
+					if (stylesheetContent) {
+						styleElement.textContent = stylesheetContent;
 					}
-				});
-			}
-
-			function removeNode(node) {
-				if ((node.nodeType != 1 || !node.querySelector("svg,style,link")) && canHideNode(node)) {
-					node.remove();
-				} else {
-					hideNode(node);
 				}
-			}
+			});
+		}
+	}
 
-			function hideNode(node) {
-				if (canHideNode(node)) {
-					node.style.setProperty("display", "none", "important");
-					Array.from(node.childNodes).forEach(removeNode);
-				}
-			}
+	removeUnselectedElements() {
+		removeUnmarkedElements(this.doc.body);
+		this.doc.body.removeAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME);
 
-			function canHideNode(node) {
+		function removeUnmarkedElements(element) {
+			let selectedElementFound = false;
+			Array.from(element.childNodes).forEach(node => {
 				if (node.nodeType == 1) {
-					const tagName = node.tagName && node.tagName.toLowerCase();
-					return (tagName != "svg" && tagName != "style" && tagName != "link");
-				}
-			}
-		}
-
-		insertVideoPosters() {
-			if (this.options.posters) {
-				this.doc.querySelectorAll("video[src], video > source[src]").forEach(element => {
-					let videoElement;
-					if (element.tagName == "VIDEO") {
-						videoElement = element;
+					const isSelectedElement = node.getAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME) == "";
+					selectedElementFound = selectedElementFound || isSelectedElement;
+					if (isSelectedElement) {
+						node.removeAttribute(util.SELECTED_CONTENT_ATTRIBUTE_NAME);
+						removeUnmarkedElements(node);
+					} else if (selectedElementFound) {
+						removeNode(node);
 					} else {
-						videoElement = element.parentElement;
+						hideNode(node);
 					}
-					const attributeValue = element.getAttribute(util.POSTER_ATTRIBUTE_NAME);
-					if (attributeValue) {
-						const posterURL = this.options.posters[Number(attributeValue)];
-						if (!videoElement.poster && posterURL) {
-							videoElement.setAttribute("poster", posterURL);
-						}
-					}
-				});
-			}
+				}
+			});
 		}
 
-		removeFrames() {
-			const frameElements = this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]");
-			this.stats.set("discarded", "frames", frameElements.length);
-			this.stats.set("processed", "frames", frameElements.length);
-			this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]").forEach(element => element.remove());
+		function removeNode(node) {
+			if ((node.nodeType != 1 || !node.querySelector("svg,style,link")) && canHideNode(node)) {
+				node.remove();
+			} else {
+				hideNode(node);
+			}
 		}
 
-		removeScripts() {
-			this.onEventAttributeNames.forEach(attributeName => this.doc.querySelectorAll("[" + attributeName + "]").forEach(element => element.removeAttribute(attributeName)));
-			this.doc.querySelectorAll("[href]").forEach(element => {
-				if (element.href && element.href.match && element.href.match(/^\s*javascript:/)) {
-					element.setAttribute("href", "");
-				}
-			});
-			this.doc.querySelectorAll("[src]").forEach(element => {
-				if (element.src && element.src.match(/^\s*javascript:/)) {
-					element.removeAttribute("src");
-				}
-			});
-			const scriptElements = this.doc.querySelectorAll("script:not([type=\"application/ld+json\"]):not([" + SCRIPT_TEMPLATE_SHADOW_ROOT + "])");
-			this.stats.set("discarded", "scripts", scriptElements.length);
-			this.stats.set("processed", "scripts", scriptElements.length);
-			scriptElements.forEach(element => element.remove());
-		}
-
-		removeVideoSources() {
-			const videoSourceElements = this.doc.querySelectorAll("video[src], video > source");
-			this.stats.set("discarded", "video sources", videoSourceElements.length);
-			this.stats.set("processed", "video sources", videoSourceElements.length);
-			videoSourceElements.forEach(element => {
-				if (element.tagName == "SOURCE") {
-					element.remove();
-				} else {
-					videoSourceElements.forEach(element => element.removeAttribute("src"));
-				}
-			});
+		function hideNode(node) {
+			if (canHideNode(node)) {
+				node.style.setProperty("display", "none", "important");
+				Array.from(node.childNodes).forEach(removeNode);
+			}
 		}
 
-		removeAudioSources() {
-			const audioSourceElements = this.doc.querySelectorAll("audio[src], audio > source[src]");
-			this.stats.set("discarded", "audio sources", audioSourceElements.length);
-			this.stats.set("processed", "audio sources", audioSourceElements.length);
-			audioSourceElements.forEach(element => {
-				if (element.tagName == "SOURCE") {
-					element.remove();
-				} else {
-					audioSourceElements.forEach(element => element.removeAttribute("src"));
-				}
-			});
+		function canHideNode(node) {
+			if (node.nodeType == 1) {
+				const tagName = node.tagName && node.tagName.toLowerCase();
+				return (tagName != "svg" && tagName != "style" && tagName != "link");
+			}
 		}
+	}
 
-		removeDiscardedResources() {
-			this.doc.querySelectorAll("." + util.SINGLE_FILE_UI_ELEMENT_CLASS).forEach(element => element.remove());
-			this.doc.querySelectorAll("meta[http-equiv=refresh], meta[disabled-http-equiv], meta[http-equiv=\"content-security-policy\"]").forEach(element => element.remove());
-			const objectElements = this.doc.querySelectorAll("applet, object[data]:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]):not([type=\"text/html\"]), embed[src]:not([src*=\".svg\"]):not([src*=\".pdf\"])");
-			this.stats.set("discarded", "objects", objectElements.length);
-			this.stats.set("processed", "objects", objectElements.length);
-			objectElements.forEach(element => element.remove());
-			const replacedAttributeValue = this.doc.querySelectorAll("link[rel~=preconnect], link[rel~=prerender], link[rel~=dns-prefetch], link[rel~=preload], link[rel~=manifest], link[rel~=prefetch]");
-			replacedAttributeValue.forEach(element => {
-				let regExp;
-				if (this.options.removeScripts) {
-					regExp = /(preconnect|prerender|dns-prefetch|preload|prefetch|manifest)/g;
+	insertVideoPosters() {
+		if (this.options.posters) {
+			this.doc.querySelectorAll("video[src], video > source[src]").forEach(element => {
+				let videoElement;
+				if (element.tagName == "VIDEO") {
+					videoElement = element;
 				} else {
-					regExp = /(preconnect|prerender|dns-prefetch|prefetch|manifest)/g;
+					videoElement = element.parentElement;
 				}
-				const relValue = element.getAttribute("rel").replace(regExp, "").trim();
-				if (relValue.length) {
-					element.setAttribute("rel", relValue);
-				} else {
-					element.remove();
+				const attributeValue = element.getAttribute(util.POSTER_ATTRIBUTE_NAME);
+				if (attributeValue) {
+					const posterURL = this.options.posters[Number(attributeValue)];
+					if (!videoElement.poster && posterURL) {
+						videoElement.setAttribute("poster", posterURL);
+					}
 				}
 			});
-			this.doc.querySelectorAll("link[rel*=stylesheet][rel*=alternate][title],link[rel*=stylesheet]:not([href]),link[rel*=stylesheet][href=\"\"]").forEach(element => element.remove());
-			if (this.options.removeHiddenElements) {
-				this.doc.querySelectorAll("input[type=hidden]").forEach(element => element.remove());
-			}
-			if (!this.options.saveFavicon) {
-				this.doc.querySelectorAll("link[rel*=\"icon\"]").forEach(element => element.remove());
-			}
-			this.doc.querySelectorAll("a[ping]").forEach(element => element.removeAttribute("ping"));
 		}
+	}
 
-		resetCharsetMeta() {
-			let charset;
-			this.doc.querySelectorAll("meta[charset], meta[http-equiv=\"content-type\"]").forEach(element => {
-				const charsetDeclaration = element.content.split(";")[1];
-				if (charsetDeclaration && !charset) {
-					charset = charsetDeclaration.split("=")[1];
-					if (charset) {
-						this.charset = charset.trim().toLowerCase();
-					}
-				}
+	removeFrames() {
+		const frameElements = this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]");
+		this.stats.set("discarded", "frames", frameElements.length);
+		this.stats.set("processed", "frames", frameElements.length);
+		this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]").forEach(element => element.remove());
+	}
+
+	removeScripts() {
+		this.onEventAttributeNames.forEach(attributeName => this.doc.querySelectorAll("[" + attributeName + "]").forEach(element => element.removeAttribute(attributeName)));
+		this.doc.querySelectorAll("[href]").forEach(element => {
+			if (element.href && element.href.match && element.href.match(/^\s*javascript:/)) {
+				element.setAttribute("href", "");
+			}
+		});
+		this.doc.querySelectorAll("[src]").forEach(element => {
+			if (element.src && element.src.match(/^\s*javascript:/)) {
+				element.removeAttribute("src");
+			}
+		});
+		const scriptElements = this.doc.querySelectorAll("script:not([type=\"application/ld+json\"]):not([" + SCRIPT_TEMPLATE_SHADOW_ROOT + "])");
+		this.stats.set("discarded", "scripts", scriptElements.length);
+		this.stats.set("processed", "scripts", scriptElements.length);
+		scriptElements.forEach(element => element.remove());
+	}
+
+	removeVideoSources() {
+		const videoSourceElements = this.doc.querySelectorAll("video[src], video > source");
+		this.stats.set("discarded", "video sources", videoSourceElements.length);
+		this.stats.set("processed", "video sources", videoSourceElements.length);
+		videoSourceElements.forEach(element => {
+			if (element.tagName == "SOURCE") {
 				element.remove();
-			});
-			const metaElement = this.doc.createElement("meta");
-			metaElement.setAttribute("charset", UTF8_CHARSET);
-			if (this.doc.head.firstChild) {
-				this.doc.head.insertBefore(metaElement, this.doc.head.firstChild);
 			} else {
-				this.doc.head.appendChild(metaElement);
+				videoSourceElements.forEach(element => element.removeAttribute("src"));
 			}
-		}
+		});
+	}
 
-		setInputValues() {
-			this.doc.querySelectorAll("input:not([type=radio]):not([type=checkbox])").forEach(input => {
-				const value = input.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
-				input.setAttribute("value", value || "");
-			});
-			this.doc.querySelectorAll("input[type=radio], input[type=checkbox]").forEach(input => {
-				const value = input.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
-				if (value == "true") {
-					input.setAttribute("checked", "");
-				}
-			});
-			this.doc.querySelectorAll("textarea").forEach(textarea => {
-				const value = textarea.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
-				textarea.textContent = value || "";
-			});
-			this.doc.querySelectorAll("select").forEach(select => {
-				select.querySelectorAll("option").forEach(option => {
-					const selected = option.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME) != null;
-					if (selected) {
-						option.setAttribute("selected", "");
-					}
-				});
-			});
-		}
+	removeAudioSources() {
+		const audioSourceElements = this.doc.querySelectorAll("audio[src], audio > source[src]");
+		this.stats.set("discarded", "audio sources", audioSourceElements.length);
+		this.stats.set("processed", "audio sources", audioSourceElements.length);
+		audioSourceElements.forEach(element => {
+			if (element.tagName == "SOURCE") {
+				element.remove();
+			} else {
+				audioSourceElements.forEach(element => element.removeAttribute("src"));
+			}
+		});
+	}
 
-		saveFavicon() {
-			let faviconElement = this.doc.querySelector("link[href][rel=\"icon\"]");
-			if (!faviconElement) {
-				faviconElement = this.doc.querySelector("link[href][rel=\"shortcut icon\"]");
+	removeDiscardedResources() {
+		this.doc.querySelectorAll("." + util.SINGLE_FILE_UI_ELEMENT_CLASS).forEach(element => element.remove());
+		this.doc.querySelectorAll("meta[http-equiv=refresh], meta[disabled-http-equiv], meta[http-equiv=\"content-security-policy\"]").forEach(element => element.remove());
+		const objectElements = this.doc.querySelectorAll("applet, object[data]:not([type=\"image/svg+xml\"]):not([type=\"image/svg-xml\"]):not([type=\"text/html\"]), embed[src]:not([src*=\".svg\"]):not([src*=\".pdf\"])");
+		this.stats.set("discarded", "objects", objectElements.length);
+		this.stats.set("processed", "objects", objectElements.length);
+		objectElements.forEach(element => element.remove());
+		const replacedAttributeValue = this.doc.querySelectorAll("link[rel~=preconnect], link[rel~=prerender], link[rel~=dns-prefetch], link[rel~=preload], link[rel~=manifest], link[rel~=prefetch]");
+		replacedAttributeValue.forEach(element => {
+			let regExp;
+			if (this.options.removeScripts) {
+				regExp = /(preconnect|prerender|dns-prefetch|preload|prefetch|manifest)/g;
+			} else {
+				regExp = /(preconnect|prerender|dns-prefetch|prefetch|manifest)/g;
 			}
-			if (!faviconElement) {
-				faviconElement = this.doc.createElement("link");
-				faviconElement.setAttribute("type", "image/x-icon");
-				faviconElement.setAttribute("rel", "shortcut icon");
-				faviconElement.setAttribute("href", "/favicon.ico");
+			const relValue = element.getAttribute("rel").replace(regExp, "").trim();
+			if (relValue.length) {
+				element.setAttribute("rel", relValue);
+			} else {
+				element.remove();
 			}
-			this.doc.head.appendChild(faviconElement);
+		});
+		this.doc.querySelectorAll("link[rel*=stylesheet][rel*=alternate][title],link[rel*=stylesheet]:not([href]),link[rel*=stylesheet][href=\"\"]").forEach(element => element.remove());
+		if (this.options.removeHiddenElements) {
+			this.doc.querySelectorAll("input[type=hidden]").forEach(element => element.remove());
+		}
+		if (!this.options.saveFavicon) {
+			this.doc.querySelectorAll("link[rel*=\"icon\"]").forEach(element => element.remove());
 		}
+		this.doc.querySelectorAll("a[ping]").forEach(element => element.removeAttribute("ping"));
+	}
 
-		replaceCanvasElements() {
-			if (this.options.canvases) {
-				this.doc.querySelectorAll("canvas").forEach(canvasElement => {
-					const attributeValue = canvasElement.getAttribute(util.CANVAS_ATTRIBUTE_NAME);
-					if (attributeValue) {
-						const canvasData = this.options.canvases[Number(attributeValue)];
-						if (canvasData) {
-							ProcessorHelper.setBackgroundImage(canvasElement, "url(" + canvasData.dataURI + ")");
-							this.stats.add("processed", "canvas", 1);
-						}
-					}
-				});
-			}
+	resetCharsetMeta() {
+		let charset;
+		this.doc.querySelectorAll("meta[charset], meta[http-equiv=\"content-type\"]").forEach(element => {
+			const charsetDeclaration = element.content.split(";")[1];
+			if (charsetDeclaration && !charset) {
+				charset = charsetDeclaration.split("=")[1];
+				if (charset) {
+					this.charset = charset.trim().toLowerCase();
+				}
+			}
+			element.remove();
+		});
+		const metaElement = this.doc.createElement("meta");
+		metaElement.setAttribute("charset", UTF8_CHARSET);
+		if (this.doc.head.firstChild) {
+			this.doc.head.insertBefore(metaElement, this.doc.head.firstChild);
+		} else {
+			this.doc.head.appendChild(metaElement);
 		}
+	}
 
-		insertFonts() {
-			if (this.options.fonts && this.options.fonts.length) {
-				let stylesheetContent = "";
-				this.options.fonts.forEach(fontData => {
-					if (fontData["font-family"] && fontData.src) {
-						stylesheetContent += "@font-face{";
-						let stylesContent = "";
-						Object.keys(fontData).forEach(fontStyle => {
-							if (stylesContent) {
-								stylesContent += ";";
-							}
-							stylesContent += fontStyle + ":" + fontData[fontStyle];
-						});
-						stylesheetContent += stylesContent + "}";
-					}
-				});
-				if (stylesheetContent) {
-					const styleElement = this.doc.createElement("style");
-					styleElement.textContent = stylesheetContent;
-					const existingStyleElement = this.doc.querySelector("style");
-					if (existingStyleElement) {
-						existingStyleElement.parentElement.insertBefore(styleElement, existingStyleElement);
-					} else {
-						this.doc.head.insertBefore(styleElement, this.doc.head.firstChild);
-					}
+	setInputValues() {
+		this.doc.querySelectorAll("input:not([type=radio]):not([type=checkbox])").forEach(input => {
+			const value = input.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
+			input.setAttribute("value", value || "");
+		});
+		this.doc.querySelectorAll("input[type=radio], input[type=checkbox]").forEach(input => {
+			const value = input.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
+			if (value == "true") {
+				input.setAttribute("checked", "");
+			}
+		});
+		this.doc.querySelectorAll("textarea").forEach(textarea => {
+			const value = textarea.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME);
+			textarea.textContent = value || "";
+		});
+		this.doc.querySelectorAll("select").forEach(select => {
+			select.querySelectorAll("option").forEach(option => {
+				const selected = option.getAttribute(util.INPUT_VALUE_ATTRIBUTE_NAME) != null;
+				if (selected) {
+					option.setAttribute("selected", "");
 				}
-			}
-		}
+			});
+		});
+	}
 
-		removeHiddenElements() {
-			const hiddenElements = this.doc.querySelectorAll("[" + util.HIDDEN_CONTENT_ATTRIBUTE_NAME + "]");
-			const removedElements = this.doc.querySelectorAll("[" + util.REMOVED_CONTENT_ATTRIBUTE_NAME + "]");
-			this.stats.set("discarded", "hidden elements", removedElements.length);
-			this.stats.set("processed", "hidden elements", removedElements.length);
-			if (hiddenElements.length) {
-				const styleElement = this.doc.createElement("style");
-				styleElement.textContent = ".sf-hidden{display:none!important;}";
-				this.doc.head.appendChild(styleElement);
-				hiddenElements.forEach(element => {
-					if (element.style.getPropertyValue("display") != "none") {
-						if (element.style.getPropertyPriority("display") == "important") {
-							element.style.setProperty("display", "none", "important");
-						} else {
-							element.classList.add("sf-hidden");
-						}
-					}
-				});
-			}
-			removedElements.forEach(element => element.remove());
+	saveFavicon() {
+		let faviconElement = this.doc.querySelector("link[href][rel=\"icon\"]");
+		if (!faviconElement) {
+			faviconElement = this.doc.querySelector("link[href][rel=\"shortcut icon\"]");
+		}
+		if (!faviconElement) {
+			faviconElement = this.doc.createElement("link");
+			faviconElement.setAttribute("type", "image/x-icon");
+			faviconElement.setAttribute("rel", "shortcut icon");
+			faviconElement.setAttribute("href", "/favicon.ico");
 		}
+		this.doc.head.appendChild(faviconElement);
+	}
 
-		resolveHrefs() {
-			this.doc.querySelectorAll("a[href], area[href], link[href]").forEach(element => {
-				const href = element.getAttribute("href").trim();
-				if (!testIgnoredPath(href)) {
-					let resolvedURL;
-					try {
-						resolvedURL = util.resolveURL(href, this.options.baseURI || this.options.url);
-					} catch (error) {
-						// ignored
-					}
-					if (resolvedURL) {
-						const url = normalizeURL(this.options.url);
-						if (resolvedURL.startsWith(url + "#") && !resolvedURL.startsWith(url + "#!") && !this.options.resolveFragmentIdentifierURLs) {
-							resolvedURL = resolvedURL.substring(url.length);
-						}
-						try {
-							element.setAttribute("href", resolvedURL);
-						} catch (error) {
-							// ignored
-						}
+	replaceCanvasElements() {
+		if (this.options.canvases) {
+			this.doc.querySelectorAll("canvas").forEach(canvasElement => {
+				const attributeValue = canvasElement.getAttribute(util.CANVAS_ATTRIBUTE_NAME);
+				if (attributeValue) {
+					const canvasData = this.options.canvases[Number(attributeValue)];
+					if (canvasData) {
+						ProcessorHelper.setBackgroundImage(canvasElement, "url(" + canvasData.dataURI + ")");
+						this.stats.add("processed", "canvas", 1);
 					}
 				}
 			});
 		}
+	}
 
-		resolveStyleAttributeURLs() {
-			this.doc.querySelectorAll("[style]").forEach(element => {
-				let styleContent = element.getAttribute("style");
-				if (this.options.compressCSS) {
-					styleContent = util.compressCSS(styleContent);
+	insertFonts() {
+		if (this.options.fonts && this.options.fonts.length) {
+			let stylesheetContent = "";
+			this.options.fonts.forEach(fontData => {
+				if (fontData["font-family"] && fontData.src) {
+					stylesheetContent += "@font-face{";
+					let stylesContent = "";
+					Object.keys(fontData).forEach(fontStyle => {
+						if (stylesContent) {
+							stylesContent += ";";
+						}
+						stylesContent += fontStyle + ":" + fontData[fontStyle];
+					});
+					stylesheetContent += stylesContent + "}";
 				}
-				styleContent = ProcessorHelper.resolveStylesheetURLs(styleContent, this.baseURI, this.workStyleElement);
-				const declarationList = cssTree.parse(styleContent, { context: "declarationList" });
-				this.styles.set(element, declarationList);
 			});
-		}
-
-		async resolveStylesheetURLs() {
-			await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]")).map(async element => {
-				const options = {
-					maxResourceSize: this.options.maxResourceSize,
-					maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
-					url: this.options.url,
-					charset: this.charset,
-					compressCSS: this.options.compressCSS,
-					updatedResources: this.options.updatedResources,
-					rootDocument: this.options.rootDocument,
-					frameId: this.options.windowId,
-					resourceReferrer: this.options.resourceReferrer
-				};
-				let mediaText;
-				if (element.media) {
-					mediaText = element.media.toLowerCase();
-				}
-				const stylesheetInfo = { mediaText };
-				if (element.closest("[" + SHADOW_MODE_ATTRIBUTE_NAME + "]")) {
-					stylesheetInfo.scoped = true;
-				}
-				if (element.tagName == "LINK" && element.charset) {
-					options.charset = element.charset;
+			if (stylesheetContent) {
+				const styleElement = this.doc.createElement("style");
+				styleElement.textContent = stylesheetContent;
+				const existingStyleElement = this.doc.querySelector("style");
+				if (existingStyleElement) {
+					existingStyleElement.parentElement.insertBefore(styleElement, existingStyleElement);
+				} else {
+					this.doc.head.insertBefore(styleElement, this.doc.head.firstChild);
 				}
-				await processElement(element, stylesheetInfo, this.stylesheets, this.baseURI, options, this.workStyleElement);
-			}));
-			if (this.options.rootDocument) {
-				const newResources = Object.keys(this.options.updatedResources).filter(url => this.options.updatedResources[url].type == "stylesheet" && !this.options.updatedResources[url].retrieved).map(url => this.options.updatedResources[url]);
-				await Promise.all(newResources.map(async resource => {
-					resource.retrieved = true;
-					const stylesheetInfo = {};
-					const element = this.doc.createElement("style");
-					this.doc.body.appendChild(element);
-					element.textContent = resource.content;
-					await processElement(element, stylesheetInfo, this.stylesheets, this.baseURI, this.options, this.workStyleElement);
-				}));
 			}
+		}
+	}
 
-			async function processElement(element, stylesheetInfo, stylesheets, baseURI, options, workStyleElement) {
-				stylesheets.set(element, stylesheetInfo);
-				let stylesheetContent = await getStylesheetContent(element, baseURI, options, workStyleElement);
-				if (!matchCharsetEquals(stylesheetContent, options.charset)) {
-					options = Object.assign({}, options, { charset: getCharset(stylesheetContent) });
-					stylesheetContent = await getStylesheetContent(element, baseURI, options, workStyleElement);
+	removeHiddenElements() {
+		const hiddenElements = this.doc.querySelectorAll("[" + util.HIDDEN_CONTENT_ATTRIBUTE_NAME + "]");
+		const removedElements = this.doc.querySelectorAll("[" + util.REMOVED_CONTENT_ATTRIBUTE_NAME + "]");
+		this.stats.set("discarded", "hidden elements", removedElements.length);
+		this.stats.set("processed", "hidden elements", removedElements.length);
+		if (hiddenElements.length) {
+			const styleElement = this.doc.createElement("style");
+			styleElement.textContent = ".sf-hidden{display:none!important;}";
+			this.doc.head.appendChild(styleElement);
+			hiddenElements.forEach(element => {
+				if (element.style.getPropertyValue("display") != "none") {
+					if (element.style.getPropertyPriority("display") == "important") {
+						element.style.setProperty("display", "none", "important");
+					} else {
+						element.classList.add("sf-hidden");
+					}
 				}
-				let stylesheet;
+			});
+		}
+		removedElements.forEach(element => element.remove());
+	}
+
+	resolveHrefs() {
+		this.doc.querySelectorAll("a[href], area[href], link[href]").forEach(element => {
+			const href = element.getAttribute("href").trim();
+			if (!testIgnoredPath(href)) {
+				let resolvedURL;
 				try {
-					stylesheet = cssTree.parse(removeCssComments(stylesheetContent));
+					resolvedURL = util.resolveURL(href, this.options.baseURI || this.options.url);
 				} catch (error) {
 					// ignored
 				}
-				if (stylesheet && stylesheet.children) {
-					if (options.compressCSS) {
-						ProcessorHelper.removeSingleLineCssComments(stylesheet);
+				if (resolvedURL) {
+					const url = normalizeURL(this.options.url);
+					if (resolvedURL.startsWith(url + "#") && !resolvedURL.startsWith(url + "#!") && !this.options.resolveFragmentIdentifierURLs) {
+						resolvedURL = resolvedURL.substring(url.length);
+					}
+					try {
+						element.setAttribute("href", resolvedURL);
+					} catch (error) {
+						// ignored
 					}
-					stylesheetInfo.stylesheet = stylesheet;
-				} else {
-					stylesheets.delete(element);
 				}
 			}
+		});
+	}
 
-			async function getStylesheetContent(element, baseURI, options, workStyleElement) {
-				let content;
-				if (element.tagName == "LINK") {
-					content = await ProcessorHelper.resolveLinkStylesheetURLs(element.href, baseURI, options, workStyleElement);
-				} else {
-					content = await ProcessorHelper.resolveImportURLs(element.textContent, baseURI, options, workStyleElement);
-				}
-				return content || "";
+	resolveStyleAttributeURLs() {
+		this.doc.querySelectorAll("[style]").forEach(element => {
+			let styleContent = element.getAttribute("style");
+			if (this.options.compressCSS) {
+				styleContent = util.compressCSS(styleContent);
 			}
+			styleContent = ProcessorHelper.resolveStylesheetURLs(styleContent, this.baseURI, this.workStyleElement);
+			const declarationList = cssTree.parse(styleContent, { context: "declarationList" });
+			this.styles.set(element, declarationList);
+		});
+	}
+
+	async resolveStylesheetURLs() {
+		await Promise.all(Array.from(this.doc.querySelectorAll("style, link[rel*=stylesheet]")).map(async element => {
+			const options = {
+				maxResourceSize: this.options.maxResourceSize,
+				maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
+				url: this.options.url,
+				charset: this.charset,
+				compressCSS: this.options.compressCSS,
+				updatedResources: this.options.updatedResources,
+				rootDocument: this.options.rootDocument,
+				frameId: this.options.windowId,
+				resourceReferrer: this.options.resourceReferrer
+			};
+			let mediaText;
+			if (element.media) {
+				mediaText = element.media.toLowerCase();
+			}
+			const stylesheetInfo = { mediaText };
+			if (element.closest("[" + SHADOW_MODE_ATTRIBUTE_NAME + "]")) {
+				stylesheetInfo.scoped = true;
+			}
+			if (element.tagName == "LINK" && element.charset) {
+				options.charset = element.charset;
+			}
+			await processElement(element, stylesheetInfo, this.stylesheets, this.baseURI, options, this.workStyleElement);
+		}));
+		if (this.options.rootDocument) {
+			const newResources = Object.keys(this.options.updatedResources).filter(url => this.options.updatedResources[url].type == "stylesheet" && !this.options.updatedResources[url].retrieved).map(url => this.options.updatedResources[url]);
+			await Promise.all(newResources.map(async resource => {
+				resource.retrieved = true;
+				const stylesheetInfo = {};
+				const element = this.doc.createElement("style");
+				this.doc.body.appendChild(element);
+				element.textContent = resource.content;
+				await processElement(element, stylesheetInfo, this.stylesheets, this.baseURI, this.options, this.workStyleElement);
+			}));
 		}
 
-		async resolveFrameURLs() {
-			if (!this.options.saveRawPage) {
-				const frameElements = Array.from(this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]"));
-				await Promise.all(frameElements.map(async frameElement => {
-					if (frameElement.tagName == "OBJECT") {
-						frameElement.setAttribute("data", "data:text/html,");
-					} else {
-						frameElement.removeAttribute("src");
-						frameElement.removeAttribute("srcdoc");
-					}
-					Array.from(frameElement.childNodes).forEach(node => node.remove());
-					const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
-					if (this.options.frames && frameWindowId) {
-						const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
-						if (frameData) {
-							await initializeProcessor(frameData, frameElement, frameWindowId, this.batchRequest, Object.create(this.options));
-						}
-					}
-				}));
+		async function processElement(element, stylesheetInfo, stylesheets, baseURI, options, workStyleElement) {
+			stylesheets.set(element, stylesheetInfo);
+			let stylesheetContent = await getStylesheetContent(element, baseURI, options, workStyleElement);
+			if (!matchCharsetEquals(stylesheetContent, options.charset)) {
+				options = Object.assign({}, options, { charset: getCharset(stylesheetContent) });
+				stylesheetContent = await getStylesheetContent(element, baseURI, options, workStyleElement);
 			}
-
-			async function initializeProcessor(frameData, frameElement, frameWindowId, batchRequest, options) {
-				options.insertSingleFileComment = false;
-				options.insertCanonicalLink = false;
-				options.insertMetaNoIndex = false;
-				options.saveFavicon = false;
-				options.url = frameData.baseURI;
-				options.windowId = frameWindowId;
-				if (frameData.content) {
-					options.content = frameData.content;
-					options.canvases = frameData.canvases;
-					options.fonts = frameData.fonts;
-					options.stylesheets = frameData.stylesheets;
-					options.images = frameData.images;
-					options.posters = frameData.posters;
-					options.usedFonts = frameData.usedFonts;
-					options.shadowRoots = frameData.shadowRoots;
-					options.imports = frameData.imports;
-					frameData.runner = new Runner(options);
-					frameData.frameElement = frameElement;
-					await frameData.runner.loadPage();
-					await frameData.runner.initialize();
-					frameData.maxResources = batchRequest.getMaxResources();
+			let stylesheet;
+			try {
+				stylesheet = cssTree.parse(removeCssComments(stylesheetContent));
+			} catch (error) {
+				// ignored
+			}
+			if (stylesheet && stylesheet.children) {
+				if (options.compressCSS) {
+					ProcessorHelper.removeSingleLineCssComments(stylesheet);
 				}
+				stylesheetInfo.stylesheet = stylesheet;
+			} else {
+				stylesheets.delete(element);
 			}
 		}
 
-		insertShadowRootContents() {
-			const doc = this.doc;
-			const options = this.options;
-			if (this.options.shadowRoots && this.options.shadowRoots.length) {
-				processElement(this.doc);
-				if (this.options.removeScripts) {
-					this.doc.querySelectorAll("script[" + SCRIPT_TEMPLATE_SHADOW_ROOT + "]").forEach(element => element.remove());
-				}
-				const scriptElement = doc.createElement("script");
-				scriptElement.setAttribute(SCRIPT_TEMPLATE_SHADOW_ROOT, "");
-				scriptElement.textContent = `(()=>{document.currentScript.remove();processNode(document);function processNode(node){node.querySelectorAll("template[${SHADOW_MODE_ATTRIBUTE_NAME}]").forEach(element=>{let shadowRoot = element.parentElement.shadowRoot;if (!shadowRoot) {try {shadowRoot=element.parentElement.attachShadow({mode:element.getAttribute("${SHADOW_MODE_ATTRIBUTE_NAME}"),delegatesFocus:Boolean(element.getAttribute("${SHADOW_DELEGATE_FOCUS_ATTRIBUTE_NAME}"))});shadowRoot.innerHTML=element.innerHTML;element.remove()} catch (error) {} if (shadowRoot) {processNode(shadowRoot)}}})}})()`;
-				doc.body.appendChild(scriptElement);
-			}
-
-			function processElement(element) {
-				const shadowRootElements = Array.from((element.querySelectorAll("[" + util.SHADOW_ROOT_ATTRIBUTE_NAME + "]")));
-				shadowRootElements.forEach(element => {
-					const attributeValue = element.getAttribute(util.SHADOW_ROOT_ATTRIBUTE_NAME);
-					if (attributeValue) {
-						const shadowRootData = options.shadowRoots[Number(attributeValue)];
-						if (shadowRootData) {
-							const templateElement = doc.createElement("template");
-							templateElement.setAttribute(SHADOW_MODE_ATTRIBUTE_NAME, shadowRootData.mode);
-							if (shadowRootData.delegatesFocus) {
-								templateElement.setAttribute(SHADOW_DELEGATE_FOCUS_ATTRIBUTE_NAME);
-							}
-							if (shadowRootData.adoptedStyleSheets) {
-								shadowRootData.adoptedStyleSheets.forEach(stylesheetContent => {
-									const styleElement = doc.createElement("style");
-									styleElement.textContent = stylesheetContent;
-									templateElement.appendChild(styleElement);
-								});
-							}
-							const shadowDoc = util.parseDocContent(shadowRootData.content);
-							if (shadowDoc.head) {
-								const metaCharset = shadowDoc.head.querySelector("meta[charset]");
-								if (metaCharset) {
-									metaCharset.remove();
-								}
-								shadowDoc.head.childNodes.forEach(node => templateElement.appendChild(shadowDoc.importNode(node, true)));
-							}
-							if (shadowDoc.body) {
-								shadowDoc.body.childNodes.forEach(node => templateElement.appendChild(shadowDoc.importNode(node, true)));
-							}
-							processElement(templateElement);
-							if (element.firstChild) {
-								element.insertBefore(templateElement, element.firstChild);
-							} else {
-								element.appendChild(templateElement);
-							}
-						}
-					}
-				});
+		async function getStylesheetContent(element, baseURI, options, workStyleElement) {
+			let content;
+			if (element.tagName == "LINK") {
+				content = await ProcessorHelper.resolveLinkStylesheetURLs(element.href, baseURI, options, workStyleElement);
+			} else {
+				content = await ProcessorHelper.resolveImportURLs(element.textContent, baseURI, options, workStyleElement);
 			}
+			return content || "";
 		}
+	}
 
-		async resolveHtmlImportURLs() {
-			const linkElements = Array.from(this.doc.querySelectorAll("link[rel=import][href]"));
-			await Promise.all(linkElements.map(async linkElement => {
-				const resourceURL = linkElement.href;
-				linkElement.removeAttribute("href");
-				const options = Object.create(this.options);
-				options.insertSingleFileComment = false;
-				options.insertCanonicalLink = false;
-				options.insertMetaNoIndex = false;
-				options.saveFavicon = false;
-				options.removeUnusedStyles = false;
-				options.removeAlternativeMedias = false;
-				options.removeUnusedFonts = false;
-				options.removeHiddenElements = false;
-				options.url = resourceURL;
-				const attributeValue = linkElement.getAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
-				if (attributeValue) {
-					const importData = options.imports[Number(attributeValue)];
-					if (importData) {
-						options.content = importData.content;
-						importData.runner = new Runner(options);
-						await importData.runner.loadPage();
-						await importData.runner.initialize();
-						if (!options.removeImports) {
-							importData.maxResources = importData.runner.batchRequest.getMaxResources();
-						}
-						importData.runner.getStyleSheets().forEach(stylesheet => {
-							const importedStyleElement = this.doc.createElement("style");
-							linkElement.insertAdjacentElement("afterEnd", importedStyleElement);
-							this.stylesheets.set(importedStyleElement, stylesheet);
-						});
+	async resolveFrameURLs() {
+		if (!this.options.saveRawPage) {
+			const frameElements = Array.from(this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]"));
+			await Promise.all(frameElements.map(async frameElement => {
+				if (frameElement.tagName == "OBJECT") {
+					frameElement.setAttribute("data", "data:text/html,");
+				} else {
+					frameElement.removeAttribute("src");
+					frameElement.removeAttribute("srcdoc");
+				}
+				Array.from(frameElement.childNodes).forEach(node => node.remove());
+				const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
+				if (this.options.frames && frameWindowId) {
+					const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
+					if (frameData) {
+						await initializeProcessor(frameData, frameElement, frameWindowId, this.batchRequest, Object.create(this.options));
 					}
 				}
-				if (options.removeImports) {
-					linkElement.remove();
-					this.stats.add("discarded", "HTML imports", 1);
-				}
 			}));
 		}
 
-		removeUnusedStyles() {
-			if (!this.mediaAllInfo) {
-				this.mediaAllInfo = util.getMediaAllInfo(this.doc, this.stylesheets, this.styles);
+		async function initializeProcessor(frameData, frameElement, frameWindowId, batchRequest, options) {
+			options.insertSingleFileComment = false;
+			options.insertCanonicalLink = false;
+			options.insertMetaNoIndex = false;
+			options.saveFavicon = false;
+			options.url = frameData.baseURI;
+			options.windowId = frameWindowId;
+			if (frameData.content) {
+				options.content = frameData.content;
+				options.canvases = frameData.canvases;
+				options.fonts = frameData.fonts;
+				options.stylesheets = frameData.stylesheets;
+				options.images = frameData.images;
+				options.posters = frameData.posters;
+				options.usedFonts = frameData.usedFonts;
+				options.shadowRoots = frameData.shadowRoots;
+				options.imports = frameData.imports;
+				frameData.runner = new Runner(options);
+				frameData.frameElement = frameElement;
+				await frameData.runner.loadPage();
+				await frameData.runner.initialize();
+				frameData.maxResources = batchRequest.getMaxResources();
 			}
-			const stats = util.minifyCSSRules(this.stylesheets, this.styles, this.mediaAllInfo);
-			this.stats.set("processed", "CSS rules", stats.processed);
-			this.stats.set("discarded", "CSS rules", stats.discarded);
 		}
+	}
 
-		removeUnusedFonts() {
-			util.removeUnusedFonts(this.doc, this.stylesheets, this.styles, this.options);
+	insertShadowRootContents() {
+		const doc = this.doc;
+		const options = this.options;
+		if (this.options.shadowRoots && this.options.shadowRoots.length) {
+			processElement(this.doc);
+			if (this.options.removeScripts) {
+				this.doc.querySelectorAll("script[" + SCRIPT_TEMPLATE_SHADOW_ROOT + "]").forEach(element => element.remove());
+			}
+			const scriptElement = doc.createElement("script");
+			scriptElement.setAttribute(SCRIPT_TEMPLATE_SHADOW_ROOT, "");
+			scriptElement.textContent = `(()=>{document.currentScript.remove();processNode(document);function processNode(node){node.querySelectorAll("template[${SHADOW_MODE_ATTRIBUTE_NAME}]").forEach(element=>{let shadowRoot = element.parentElement.shadowRoot;if (!shadowRoot) {try {shadowRoot=element.parentElement.attachShadow({mode:element.getAttribute("${SHADOW_MODE_ATTRIBUTE_NAME}"),delegatesFocus:Boolean(element.getAttribute("${SHADOW_DELEGATE_FOCUS_ATTRIBUTE_NAME}"))});shadowRoot.innerHTML=element.innerHTML;element.remove()} catch (error) {} if (shadowRoot) {processNode(shadowRoot)}}})}})()`;
+			doc.body.appendChild(scriptElement);
 		}
 
-		removeAlternativeMedias() {
-			const stats = util.minifyMedias(this.stylesheets);
-			this.stats.set("processed", "medias", stats.processed);
-			this.stats.set("discarded", "medias", stats.discarded);
+		function processElement(element) {
+			const shadowRootElements = Array.from((element.querySelectorAll("[" + util.SHADOW_ROOT_ATTRIBUTE_NAME + "]")));
+			shadowRootElements.forEach(element => {
+				const attributeValue = element.getAttribute(util.SHADOW_ROOT_ATTRIBUTE_NAME);
+				if (attributeValue) {
+					const shadowRootData = options.shadowRoots[Number(attributeValue)];
+					if (shadowRootData) {
+						const templateElement = doc.createElement("template");
+						templateElement.setAttribute(SHADOW_MODE_ATTRIBUTE_NAME, shadowRootData.mode);
+						if (shadowRootData.delegatesFocus) {
+							templateElement.setAttribute(SHADOW_DELEGATE_FOCUS_ATTRIBUTE_NAME);
+						}
+						if (shadowRootData.adoptedStyleSheets) {
+							shadowRootData.adoptedStyleSheets.forEach(stylesheetContent => {
+								const styleElement = doc.createElement("style");
+								styleElement.textContent = stylesheetContent;
+								templateElement.appendChild(styleElement);
+							});
+						}
+						const shadowDoc = util.parseDocContent(shadowRootData.content);
+						if (shadowDoc.head) {
+							const metaCharset = shadowDoc.head.querySelector("meta[charset]");
+							if (metaCharset) {
+								metaCharset.remove();
+							}
+							shadowDoc.head.childNodes.forEach(node => templateElement.appendChild(shadowDoc.importNode(node, true)));
+						}
+						if (shadowDoc.body) {
+							shadowDoc.body.childNodes.forEach(node => templateElement.appendChild(shadowDoc.importNode(node, true)));
+						}
+						processElement(templateElement);
+						if (element.firstChild) {
+							element.insertBefore(templateElement, element.firstChild);
+						} else {
+							element.appendChild(templateElement);
+						}
+					}
+				}
+			});
 		}
+	}
 
-		async processStylesheets() {
-			this.options.fontURLs = new Map();
-			await Promise.all([...this.stylesheets].map(([, stylesheetInfo]) =>
-				ProcessorHelper.processStylesheet(stylesheetInfo.stylesheet.children, this.baseURI, this.options, this.cssVariables, this.batchRequest)
-			));
-		}
+	async resolveHtmlImportURLs() {
+		const linkElements = Array.from(this.doc.querySelectorAll("link[rel=import][href]"));
+		await Promise.all(linkElements.map(async linkElement => {
+			const resourceURL = linkElement.href;
+			linkElement.removeAttribute("href");
+			const options = Object.create(this.options);
+			options.insertSingleFileComment = false;
+			options.insertCanonicalLink = false;
+			options.insertMetaNoIndex = false;
+			options.saveFavicon = false;
+			options.removeUnusedStyles = false;
+			options.removeAlternativeMedias = false;
+			options.removeUnusedFonts = false;
+			options.removeHiddenElements = false;
+			options.url = resourceURL;
+			const attributeValue = linkElement.getAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
+			if (attributeValue) {
+				const importData = options.imports[Number(attributeValue)];
+				if (importData) {
+					options.content = importData.content;
+					importData.runner = new Runner(options);
+					await importData.runner.loadPage();
+					await importData.runner.initialize();
+					if (!options.removeImports) {
+						importData.maxResources = importData.runner.batchRequest.getMaxResources();
+					}
+					importData.runner.getStyleSheets().forEach(stylesheet => {
+						const importedStyleElement = this.doc.createElement("style");
+						linkElement.insertAdjacentElement("afterEnd", importedStyleElement);
+						this.stylesheets.set(importedStyleElement, stylesheet);
+					});
+				}
+			}
+			if (options.removeImports) {
+				linkElement.remove();
+				this.stats.add("discarded", "HTML imports", 1);
+			}
+		}));
+	}
 
-		async processStyleAttributes() {
-			return Promise.all([...this.styles].map(([, declarationList]) =>
-				ProcessorHelper.processStyle(declarationList.children.toArray(), this.baseURI, this.options, this.cssVariables, this.batchRequest)
-			));
+	removeUnusedStyles() {
+		if (!this.mediaAllInfo) {
+			this.mediaAllInfo = util.getMediaAllInfo(this.doc, this.stylesheets, this.styles);
 		}
+		const stats = util.minifyCSSRules(this.stylesheets, this.styles, this.mediaAllInfo);
+		this.stats.set("processed", "CSS rules", stats.processed);
+		this.stats.set("discarded", "CSS rules", stats.discarded);
+	}
 
-		async processPageResources() {
-			const processAttributeArgs = [
-				["link[href][rel*=\"icon\"]", "href", false, true],
-				["object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]", "data"],
-				["img[src], input[src][type=image]", "src", true],
-				["embed[src*=\".svg\"], embed[src*=\".pdf\"]", "src"],
-				["video[poster]", "poster"],
-				["*[background]", "background"],
-				["image", "xlink:href"],
-				["image", "href"]
-			];
-			let resourcePromises = processAttributeArgs.map(([selector, attributeName, processDuplicates, removeElementIfMissing]) =>
-				ProcessorHelper.processAttribute(this.doc.querySelectorAll(selector), attributeName, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest, processDuplicates, removeElementIfMissing)
-			);
-			resourcePromises = resourcePromises.concat([
-				ProcessorHelper.processXLinks(this.doc.querySelectorAll("use"), this.baseURI, this.options, this.batchRequest),
-				ProcessorHelper.processSrcset(this.doc.querySelectorAll("img[srcset], source[srcset]"), "srcset", this.baseURI, this.batchRequest)
-			]);
-			if (!this.options.removeAudioSrc) {
-				resourcePromises.push(ProcessorHelper.processAttribute(this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest));
-			}
-			if (!this.options.removeVideoSrc) {
-				resourcePromises.push(ProcessorHelper.processAttribute(this.doc.querySelectorAll("video[src], video > source[src]"), "src", this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest));
-			}
-			await Promise.all(resourcePromises);
-			if (this.options.saveFavicon) {
-				ProcessorHelper.processShortcutIcons(this.doc);
-			}
+	removeUnusedFonts() {
+		util.removeUnusedFonts(this.doc, this.stylesheets, this.styles, this.options);
+	}
+
+	removeAlternativeMedias() {
+		const stats = util.minifyMedias(this.stylesheets);
+		this.stats.set("processed", "medias", stats.processed);
+		this.stats.set("discarded", "medias", stats.discarded);
+	}
+
+	async processStylesheets() {
+		this.options.fontURLs = new Map();
+		await Promise.all([...this.stylesheets].map(([, stylesheetInfo]) =>
+			ProcessorHelper.processStylesheet(stylesheetInfo.stylesheet.children, this.baseURI, this.options, this.cssVariables, this.batchRequest)
+		));
+	}
+
+	async processStyleAttributes() {
+		return Promise.all([...this.styles].map(([, declarationList]) =>
+			ProcessorHelper.processStyle(declarationList.children.toArray(), this.baseURI, this.options, this.cssVariables, this.batchRequest)
+		));
+	}
+
+	async processPageResources() {
+		const processAttributeArgs = [
+			["link[href][rel*=\"icon\"]", "href", false, true],
+			["object[type=\"image/svg+xml\"], object[type=\"image/svg-xml\"]", "data"],
+			["img[src], input[src][type=image]", "src", true],
+			["embed[src*=\".svg\"], embed[src*=\".pdf\"]", "src"],
+			["video[poster]", "poster"],
+			["*[background]", "background"],
+			["image", "xlink:href"],
+			["image", "href"]
+		];
+		let resourcePromises = processAttributeArgs.map(([selector, attributeName, processDuplicates, removeElementIfMissing]) =>
+			ProcessorHelper.processAttribute(this.doc.querySelectorAll(selector), attributeName, this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest, processDuplicates, removeElementIfMissing)
+		);
+		resourcePromises = resourcePromises.concat([
+			ProcessorHelper.processXLinks(this.doc.querySelectorAll("use"), this.baseURI, this.options, this.batchRequest),
+			ProcessorHelper.processSrcset(this.doc.querySelectorAll("img[srcset], source[srcset]"), "srcset", this.baseURI, this.batchRequest)
+		]);
+		if (!this.options.removeAudioSrc) {
+			resourcePromises.push(ProcessorHelper.processAttribute(this.doc.querySelectorAll("audio[src], audio > source[src]"), "src", this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest));
+		}
+		if (!this.options.removeVideoSrc) {
+			resourcePromises.push(ProcessorHelper.processAttribute(this.doc.querySelectorAll("video[src], video > source[src]"), "src", this.baseURI, this.options, this.cssVariables, this.styles, this.batchRequest));
+		}
+		await Promise.all(resourcePromises);
+		if (this.options.saveFavicon) {
+			ProcessorHelper.processShortcutIcons(this.doc);
 		}
+	}
 
-		async processScripts() {
-			await Promise.all(Array.from(this.doc.querySelectorAll("script[src], link[rel=preload][as=script][href]")).map(async element => {
-				let resourceURL;
-				let scriptSrc;
+	async processScripts() {
+		await Promise.all(Array.from(this.doc.querySelectorAll("script[src], link[rel=preload][as=script][href]")).map(async element => {
+			let resourceURL;
+			let scriptSrc;
+			if (element.tagName == "SCRIPT") {
+				scriptSrc = element.getAttribute("src");
+				element.removeAttribute("src");
+			} else {
+				scriptSrc = element.getAttribute("href");
+				element.removeAttribute("href");
+			}
+			element.removeAttribute("integrity");
+			element.textContent = "";
+			try {
+				resourceURL = util.resolveURL(scriptSrc, this.baseURI);
+			} catch (error) {
+				// ignored
+			}
+			if (testValidURL(resourceURL)) {
+				this.stats.add("processed", "scripts", 1);
+				const content = await util.getContent(resourceURL, {
+					asBinary: true,
+					charset: this.charset != UTF8_CHARSET && this.charset,
+					maxResourceSize: this.options.maxResourceSize,
+					maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
+					frameId: this.options.windowId,
+					resourceReferrer: this.options.resourceReferrer
+				});
+				content.data = getUpdatedResourceContent(resourceURL, content, this.options);
 				if (element.tagName == "SCRIPT") {
-					scriptSrc = element.getAttribute("src");
-					element.removeAttribute("src");
-				} else {
-					scriptSrc = element.getAttribute("href");
-					element.removeAttribute("href");
-				}
-				element.removeAttribute("integrity");
-				element.textContent = "";
-				try {
-					resourceURL = util.resolveURL(scriptSrc, this.baseURI);
-				} catch (error) {
-					// ignored
-				}
-				if (testValidURL(resourceURL)) {
-					this.stats.add("processed", "scripts", 1);
-					const content = await util.getContent(resourceURL, {
-						asBinary: true,
-						charset: this.charset != UTF8_CHARSET && this.charset,
-						maxResourceSize: this.options.maxResourceSize,
-						maxResourceSizeEnabled: this.options.maxResourceSizeEnabled,
-						frameId: this.options.windowId,
-						resourceReferrer: this.options.resourceReferrer
-					});
-					content.data = getUpdatedResourceContent(resourceURL, content, this.options);
-					if (element.tagName == "SCRIPT") {
-						element.setAttribute("src", content.data);
-						if (element.getAttribute("async") == "" || element.getAttribute("async") == "async" || element.getAttribute(util.ASYNC_SCRIPT_ATTRIBUTE_NAME) == "") {
-							element.setAttribute("async", "");
-						}
-					} else {
-						const scriptElement = this.doc.createElement("script");
-						scriptElement.setAttribute("src", content.data);
-						scriptElement.setAttribute("async", "");
-						element.parentElement.replaceChild(scriptElement, element);
+					element.setAttribute("src", content.data);
+					if (element.getAttribute("async") == "" || element.getAttribute("async") == "async" || element.getAttribute(util.ASYNC_SCRIPT_ATTRIBUTE_NAME) == "") {
+						element.setAttribute("async", "");
 					}
+				} else {
+					const scriptElement = this.doc.createElement("script");
+					scriptElement.setAttribute("src", content.data);
+					scriptElement.setAttribute("async", "");
+					element.parentElement.replaceChild(scriptElement, element);
 				}
-			}));
-		}
+			}
+		}));
+	}
 
-		removeAlternativeImages() {
-			util.removeAlternativeImages(this.doc);
-		}
+	removeAlternativeImages() {
+		util.removeAlternativeImages(this.doc);
+	}
 
-		async removeAlternativeFonts() {
-			await util.removeAlternativeFonts(this.doc, this.stylesheets, this.options.fontURLs, this.options.fontTests);
-		}
+	async removeAlternativeFonts() {
+		await util.removeAlternativeFonts(this.doc, this.stylesheets, this.options.fontURLs, this.options.fontTests);
+	}
 
-		async processFrames() {
-			if (this.options.frames) {
-				const frameElements = Array.from(this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]"));
-				await Promise.all(frameElements.map(async frameElement => {
-					const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
-					if (frameWindowId) {
-						const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
-						if (frameData) {
-							this.options.frames = this.options.frames.filter(frame => frame.windowId != frameWindowId);
-							if (frameData.runner && frameElement.getAttribute(util.HIDDEN_FRAME_ATTRIBUTE_NAME) != "") {
-								this.stats.add("processed", "frames", 1);
-								await frameData.runner.run();
-								const pageData = await frameData.runner.getPageData();
-								frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
-								let sandbox = "allow-popups allow-top-navigation allow-top-navigation-by-user-activation";
-								if (pageData.content.match(NOSCRIPT_TAG_FOUND) || pageData.content.match(SCRIPT_TAG_FOUND)) {
-									sandbox += " allow-scripts allow-same-origin";
-								}
-								frameElement.setAttribute("sandbox", sandbox);
-								if (frameElement.tagName == "OBJECT") {
-									frameElement.setAttribute("data", "data:text/html," + pageData.content);
+	async processFrames() {
+		if (this.options.frames) {
+			const frameElements = Array.from(this.doc.querySelectorAll("iframe, frame, object[type=\"text/html\"][data]"));
+			await Promise.all(frameElements.map(async frameElement => {
+				const frameWindowId = frameElement.getAttribute(util.WIN_ID_ATTRIBUTE_NAME);
+				if (frameWindowId) {
+					const frameData = this.options.frames.find(frame => frame.windowId == frameWindowId);
+					if (frameData) {
+						this.options.frames = this.options.frames.filter(frame => frame.windowId != frameWindowId);
+						if (frameData.runner && frameElement.getAttribute(util.HIDDEN_FRAME_ATTRIBUTE_NAME) != "") {
+							this.stats.add("processed", "frames", 1);
+							await frameData.runner.run();
+							const pageData = await frameData.runner.getPageData();
+							frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
+							let sandbox = "allow-popups allow-top-navigation allow-top-navigation-by-user-activation";
+							if (pageData.content.match(NOSCRIPT_TAG_FOUND) || pageData.content.match(SCRIPT_TAG_FOUND)) {
+								sandbox += " allow-scripts allow-same-origin";
+							}
+							frameElement.setAttribute("sandbox", sandbox);
+							if (frameElement.tagName == "OBJECT") {
+								frameElement.setAttribute("data", "data:text/html," + pageData.content);
+							} else {
+								if (frameElement.tagName == "FRAME") {
+									frameElement.setAttribute("src", "data:text/html," + pageData.content.replace(/%/g, "%25").replace(/#/g, "%23"));
 								} else {
-									if (frameElement.tagName == "FRAME") {
-										frameElement.setAttribute("src", "data:text/html," + pageData.content.replace(/%/g, "%25").replace(/#/g, "%23"));
-									} else {
-										frameElement.setAttribute("srcdoc", pageData.content);
-										frameElement.removeAttribute("src");
-									}
+									frameElement.setAttribute("srcdoc", pageData.content);
+									frameElement.removeAttribute("src");
 								}
-								this.stats.addAll(pageData);
-							} else {
-								frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
-								this.stats.add("discarded", "frames", 1);
 							}
+							this.stats.addAll(pageData);
+						} else {
+							frameElement.removeAttribute(util.WIN_ID_ATTRIBUTE_NAME);
+							this.stats.add("discarded", "frames", 1);
 						}
 					}
-				}));
-			}
-		}
-
-		async processHtmlImports() {
-			const linkElements = Array.from(this.doc.querySelectorAll("link[rel=import]"));
-			await Promise.all(linkElements.map(async linkElement => {
-				const attributeValue = linkElement.getAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
-				if (attributeValue) {
-					const importData = this.options.imports[Number(attributeValue)];
-					if (importData.runner) {
-						this.stats.add("processed", "HTML imports", 1);
-						await importData.runner.run();
-						const pageData = await importData.runner.getPageData();
-						linkElement.removeAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
-						linkElement.setAttribute("href", "data:text/html," + pageData.content);
-						this.stats.addAll(pageData);
-					} else {
-						this.stats.add("discarded", "HTML imports", 1);
-					}
 				}
 			}));
 		}
+	}
 
-		replaceStylesheets() {
-			this.doc.querySelectorAll("style").forEach(styleElement => {
-				const stylesheetInfo = this.stylesheets.get(styleElement);
-				if (stylesheetInfo) {
-					this.stylesheets.delete(styleElement);
-					let stylesheetContent = cssTree.generate(stylesheetInfo.stylesheet);
-					styleElement.textContent = stylesheetContent;
-					if (stylesheetInfo.mediaText) {
-						styleElement.media = stylesheetInfo.mediaText;
-					}
+	async processHtmlImports() {
+		const linkElements = Array.from(this.doc.querySelectorAll("link[rel=import]"));
+		await Promise.all(linkElements.map(async linkElement => {
+			const attributeValue = linkElement.getAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
+			if (attributeValue) {
+				const importData = this.options.imports[Number(attributeValue)];
+				if (importData.runner) {
+					this.stats.add("processed", "HTML imports", 1);
+					await importData.runner.run();
+					const pageData = await importData.runner.getPageData();
+					linkElement.removeAttribute(util.HTML_IMPORT_ATTRIBUTE_NAME);
+					linkElement.setAttribute("href", "data:text/html," + pageData.content);
+					this.stats.addAll(pageData);
 				} else {
-					styleElement.remove();
+					this.stats.add("discarded", "HTML imports", 1);
 				}
-			});
-			this.doc.querySelectorAll("link[rel*=stylesheet]").forEach(linkElement => {
-				const stylesheetInfo = this.stylesheets.get(linkElement);
-				if (stylesheetInfo) {
-					this.stylesheets.delete(linkElement);
-					const styleElement = this.doc.createElement("style");
-					if (stylesheetInfo.mediaText) {
-						styleElement.media = stylesheetInfo.mediaText;
-					}
-					let stylesheetContent = cssTree.generate(stylesheetInfo.stylesheet);
-					styleElement.textContent = stylesheetContent;
-					linkElement.parentElement.replaceChild(styleElement, linkElement);
-				} else {
-					linkElement.remove();
+			}
+		}));
+	}
+
+	replaceStylesheets() {
+		this.doc.querySelectorAll("style").forEach(styleElement => {
+			const stylesheetInfo = this.stylesheets.get(styleElement);
+			if (stylesheetInfo) {
+				this.stylesheets.delete(styleElement);
+				let stylesheetContent = cssTree.generate(stylesheetInfo.stylesheet);
+				styleElement.textContent = stylesheetContent;
+				if (stylesheetInfo.mediaText) {
+					styleElement.media = stylesheetInfo.mediaText;
+				}
+			} else {
+				styleElement.remove();
+			}
+		});
+		this.doc.querySelectorAll("link[rel*=stylesheet]").forEach(linkElement => {
+			const stylesheetInfo = this.stylesheets.get(linkElement);
+			if (stylesheetInfo) {
+				this.stylesheets.delete(linkElement);
+				const styleElement = this.doc.createElement("style");
+				if (stylesheetInfo.mediaText) {
+					styleElement.media = stylesheetInfo.mediaText;
+				}
+				let stylesheetContent = cssTree.generate(stylesheetInfo.stylesheet);
+				styleElement.textContent = stylesheetContent;
+				linkElement.parentElement.replaceChild(styleElement, linkElement);
+			} else {
+				linkElement.remove();
+			}
+		});
+	}
+
+	replaceStyleAttributes() {
+		this.doc.querySelectorAll("[style]").forEach(element => {
+			const declarations = this.styles.get(element);
+			if (declarations) {
+				this.styles.delete(element);
+				let styleContent = cssTree.generate(declarations);
+				element.setAttribute("style", styleContent);
+			} else {
+				element.setAttribute("style", "");
+			}
+		});
+	}
+
+	insertVariables() {
+		if (this.cssVariables.size) {
+			const styleElement = this.doc.createElement("style");
+			const firstStyleElement = this.doc.head.querySelector("style");
+			if (firstStyleElement) {
+				this.doc.head.insertBefore(styleElement, firstStyleElement);
+			} else {
+				this.doc.head.appendChild(styleElement);
+			}
+			let stylesheetContent = "";
+			this.cssVariables.forEach((content, indexResource) => {
+				this.cssVariables.delete(indexResource);
+				if (stylesheetContent) {
+					stylesheetContent += ";";
 				}
+				stylesheetContent += `${SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource}:url("${content}")`;
 			});
+			styleElement.textContent = ":root{" + stylesheetContent + "}";
 		}
+	}
 
-		replaceStyleAttributes() {
-			this.doc.querySelectorAll("[style]").forEach(element => {
-				const declarations = this.styles.get(element);
-				if (declarations) {
-					this.styles.delete(element);
-					let styleContent = cssTree.generate(declarations);
-					element.setAttribute("style", styleContent);
-				} else {
-					element.setAttribute("style", "");
-				}
-			});
+	compressHTML() {
+		let size;
+		if (this.options.displayStats) {
+			size = util.getContentSize(this.doc.documentElement.outerHTML);
 		}
-
-		insertVariables() {
-			if (this.cssVariables.size) {
-				const styleElement = this.doc.createElement("style");
-				const firstStyleElement = this.doc.head.querySelector("style");
-				if (firstStyleElement) {
-					this.doc.head.insertBefore(styleElement, firstStyleElement);
-				} else {
-					this.doc.head.appendChild(styleElement);
-				}
-				let stylesheetContent = "";
-				this.cssVariables.forEach((content, indexResource) => {
-					this.cssVariables.delete(indexResource);
-					if (stylesheetContent) {
-						stylesheetContent += ";";
-					}
-					stylesheetContent += `${SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource}:url("${content}")`;
-				});
-				styleElement.textContent = ":root{" + stylesheetContent + "}";
-			}
+		util.minifyHTML(this.doc, { PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME: util.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME });
+		if (this.options.displayStats) {
+			this.stats.add("discarded", "HTML bytes", size - util.getContentSize(this.doc.documentElement.outerHTML));
 		}
+	}
 
-		compressHTML() {
-			let size;
-			if (this.options.displayStats) {
-				size = util.getContentSize(this.doc.documentElement.outerHTML);
-			}
-			util.minifyHTML(this.doc, { PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME: util.PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME });
-			if (this.options.displayStats) {
-				this.stats.add("discarded", "HTML bytes", size - util.getContentSize(this.doc.documentElement.outerHTML));
+	cleanupPage() {
+		this.doc.querySelectorAll("base").forEach(element => element.remove());
+		const metaCharset = this.doc.head.querySelector("meta[charset]");
+		if (metaCharset) {
+			this.doc.head.insertBefore(metaCharset, this.doc.head.firstChild);
+			if (this.doc.head.querySelectorAll("*").length == 1 && this.doc.body.childNodes.length == 0) {
+				this.doc.head.querySelector("meta[charset]").remove();
 			}
 		}
+	}
 
-		cleanupPage() {
-			this.doc.querySelectorAll("base").forEach(element => element.remove());
-			const metaCharset = this.doc.head.querySelector("meta[charset]");
-			if (metaCharset) {
-				this.doc.head.insertBefore(metaCharset, this.doc.head.firstChild);
-				if (this.doc.head.querySelectorAll("*").length == 1 && this.doc.body.childNodes.length == 0) {
-					this.doc.head.querySelector("meta[charset]").remove();
-				}
-			}
-		}
+	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");
+		const minHeight = this.doc.documentElement.style.getPropertyValue("-sf-min-height");
+		const minHeightPriority = this.doc.documentElement.style.getPropertyPriority("-sf-min-height");
+		this.doc.documentElement.style.setProperty("transform", transform, transformPriority);
+		this.doc.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
+		this.doc.documentElement.style.setProperty("min-height", minHeight, minHeightPriority);
+		this.doc.documentElement.style.removeProperty("-sf-transform");
+		this.doc.documentElement.style.removeProperty("-sf-transform-origin");
+		this.doc.documentElement.style.removeProperty("-sf-min-height");
+	}
 
-		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");
-			const minHeight = this.doc.documentElement.style.getPropertyValue("-sf-min-height");
-			const minHeightPriority = this.doc.documentElement.style.getPropertyPriority("-sf-min-height");
-			this.doc.documentElement.style.setProperty("transform", transform, transformPriority);
-			this.doc.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
-			this.doc.documentElement.style.setProperty("min-height", minHeight, minHeightPriority);
-			this.doc.documentElement.style.removeProperty("-sf-transform");
-			this.doc.documentElement.style.removeProperty("-sf-transform-origin");
-			this.doc.documentElement.style.removeProperty("-sf-min-height");
-		}
-
-		async insertMAFFMetaData() {
-			const maffMetaData = await this.maffMetaDataPromise;
-			if (maffMetaData && maffMetaData.content) {
-				const NAMESPACE_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
-				const maffDoc = util.parseXMLContent(maffMetaData.content);
-				const originalURLElement = maffDoc.querySelector("RDF > Description > originalurl");
-				const archiveTimeElement = maffDoc.querySelector("RDF > Description > archivetime");
-				if (originalURLElement) {
-					this.options.saveUrl = originalURLElement.getAttributeNS(NAMESPACE_RDF, "resource");
-				}
-				if (archiveTimeElement) {
-					const value = archiveTimeElement.getAttributeNS(NAMESPACE_RDF, "resource");
-					if (value) {
-						const date = new Date(value);
-						if (!isNaN(date.getTime())) {
-							this.options.saveDate = new Date(value);
-						}
+	async insertMAFFMetaData() {
+		const maffMetaData = await this.maffMetaDataPromise;
+		if (maffMetaData && maffMetaData.content) {
+			const NAMESPACE_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+			const maffDoc = util.parseXMLContent(maffMetaData.content);
+			const originalURLElement = maffDoc.querySelector("RDF > Description > originalurl");
+			const archiveTimeElement = maffDoc.querySelector("RDF > Description > archivetime");
+			if (originalURLElement) {
+				this.options.saveUrl = originalURLElement.getAttributeNS(NAMESPACE_RDF, "resource");
+			}
+			if (archiveTimeElement) {
+				const value = archiveTimeElement.getAttributeNS(NAMESPACE_RDF, "resource");
+				if (value) {
+					const date = new Date(value);
+					if (!isNaN(date.getTime())) {
+						this.options.saveDate = new Date(value);
 					}
 				}
 			}
 		}
+	}
 
-		async setDocInfo() {
-			const titleElement = this.doc.querySelector("title");
-			const descriptionElement = this.doc.querySelector("meta[name=description]");
-			const authorElement = this.doc.querySelector("meta[name=author]");
-			const creatorElement = this.doc.querySelector("meta[name=creator]");
-			const publisherElement = this.doc.querySelector("meta[name=publisher]");
-			const headingElement = this.doc.querySelector("h1");
-			this.options.title = titleElement ? titleElement.textContent.trim() : "";
-			this.options.info = {
-				description: descriptionElement && descriptionElement.content ? descriptionElement.content.trim() : "",
-				lang: this.doc.documentElement.lang,
-				author: authorElement && authorElement.content ? authorElement.content.trim() : "",
-				creator: creatorElement && creatorElement.content ? creatorElement.content.trim() : "",
-				publisher: publisherElement && publisherElement.content ? publisherElement.content.trim() : "",
-				heading: headingElement && headingElement.textContent ? headingElement.textContent.trim() : ""
-			};
-			this.options.infobarContent = await ProcessorHelper.evalTemplate(this.options.infobarTemplate, this.options, null, true);
-		}
-	}
-
-	// ---------------
-	// ProcessorHelper
-	// ---------------
-	const DATA_URI_PREFIX = "data:";
-	const ABOUT_BLANK_URI = "about:blank";
-	const EMPTY_DATA_URI = "data:null;base64,";
-	const REGEXP_URL_HASH = /(#.+?)$/;
-	const SINGLE_FILE_VARIABLE_NAME_PREFIX = "--sf-img-";
-	const SINGLE_FILE_VARIABLE_MAX_SIZE = 512 * 1024;
-
-	class ProcessorHelper {
-		static async evalTemplate(template = "", options, content, dontReplaceSlash) {
-			const url = util.parseURL(options.saveUrl);
-			template = await evalTemplateVariable(template, "page-title", () => options.title || "No title", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "page-heading", () => options.info.heading || "No heading", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "page-language", () => options.info.lang || "No language", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "page-description", () => options.info.description || "No description", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "page-author", () => options.info.author || "No author", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "page-creator", () => options.info.creator || "No creator", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "page-publisher", () => options.info.publisher || "No publisher", dontReplaceSlash, options.filenameReplacementCharacter);
-			await evalDate(options.saveDate);
-			await evalDate(options.visitDate, "visit-");
-			template = await evalTemplateVariable(template, "url-hash", () => url.hash.substring(1) || "No hash", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-host", () => url.host.replace(/\/$/, "") || "No host", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-hostname", () => url.hostname.replace(/\/$/, "") || "No hostname", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-href", () => decode(url.href) || "No href", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-href-flat", () => decode(url.href) || "No href", false, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-referrer", () => decode(options.referrer) || "No referrer", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-referrer-flat", () => decode(options.referrer) || "No referrer", false, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-password", () => url.password || "No password", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-pathname", () => decode(url.pathname).replace(/^\//, "").replace(/\/$/, "") || "No pathname", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-pathname-flat", () => decode(url.pathname) || "No pathname", false, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-port", () => url.port || "No port", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-protocol", () => url.protocol || "No protocol", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-search", () => url.search.substring(1) || "No search", dontReplaceSlash, options.filenameReplacementCharacter);
-			const params = url.search.substring(1).split("&").map(parameter => parameter.split("="));
-			for (const [name, value] of params) {
-				template = await evalTemplateVariable(template, "url-search-" + name, () => value || "", dontReplaceSlash, options.filenameReplacementCharacter);
-			}
-			template = template.replace(/{\s*url-search-[^}\s]*\s*}/gi, "");
-			template = await evalTemplateVariable(template, "url-username", () => url.username || "No username", dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "tab-id", () => String(options.tabId || "No tab id"), dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "tab-index", () => String(options.tabIndex || "No tab index"), dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "url-last-segment", () => decode(getLastSegment(url, options.filenameReplacementCharacter)) || "No last segment", dontReplaceSlash, options.filenameReplacementCharacter);
-			if (content) {
-				template = await evalTemplateVariable(template, "digest-sha-256", async () => util.digest("SHA-256", content), dontReplaceSlash, options.filenameReplacementCharacter);
-				template = await evalTemplateVariable(template, "digest-sha-384", async () => util.digest("SHA-384", content), dontReplaceSlash, options.filenameReplacementCharacter);
-				template = await evalTemplateVariable(template, "digest-sha-512", async () => util.digest("SHA-512", content), dontReplaceSlash, options.filenameReplacementCharacter);
-			}
-			const bookmarkFolder = (options.bookmarkFolders && options.bookmarkFolders.join("/")) || "";
-			template = await evalTemplateVariable(template, "bookmark-pathname", () => bookmarkFolder, dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "bookmark-pathname-flat", () => bookmarkFolder, false, options.filenameReplacementCharacter);
-			template = await evalTemplateVariable(template, "profile-name", () => options.profileName, dontReplaceSlash, options.filenameReplacementCharacter);
-			return template.trim();
-
-			function decode(value) {
-				try {
-					return decodeURI(value);
-				} catch (error) {
-					return value;
-				}
-			}
-
-			async function evalDate(date, prefix = "") {
-				if (date) {
-					template = await evalTemplateVariable(template, prefix + "datetime-iso", () => date.toISOString(), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "date-iso", () => date.toISOString().split("T")[0], dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "time-iso", () => date.toISOString().split("T")[1].split("Z")[0], dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "date-locale", () => date.toLocaleDateString(), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "time-locale", () => date.toLocaleTimeString(), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "day-locale", () => String(date.getDate()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "month-locale", () => String(date.getMonth() + 1).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "year-locale", () => String(date.getFullYear()), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "datetime-locale", () => date.toLocaleString(), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "datetime-utc", () => date.toUTCString(), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "day-utc", () => String(date.getUTCDate()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "month-utc", () => String(date.getUTCMonth() + 1).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "year-utc", () => String(date.getUTCFullYear()), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "hours-locale", () => String(date.getHours()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "minutes-locale", () => String(date.getMinutes()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "seconds-locale", () => String(date.getSeconds()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "hours-utc", () => String(date.getUTCHours()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "minutes-utc", () => String(date.getUTCMinutes()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "seconds-utc", () => String(date.getUTCSeconds()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
-					template = await evalTemplateVariable(template, prefix + "time-ms", () => String(date.getTime()), dontReplaceSlash, options.filenameReplacementCharacter);
-				}
+	async setDocInfo() {
+		const titleElement = this.doc.querySelector("title");
+		const descriptionElement = this.doc.querySelector("meta[name=description]");
+		const authorElement = this.doc.querySelector("meta[name=author]");
+		const creatorElement = this.doc.querySelector("meta[name=creator]");
+		const publisherElement = this.doc.querySelector("meta[name=publisher]");
+		const headingElement = this.doc.querySelector("h1");
+		this.options.title = titleElement ? titleElement.textContent.trim() : "";
+		this.options.info = {
+			description: descriptionElement && descriptionElement.content ? descriptionElement.content.trim() : "",
+			lang: this.doc.documentElement.lang,
+			author: authorElement && authorElement.content ? authorElement.content.trim() : "",
+			creator: creatorElement && creatorElement.content ? creatorElement.content.trim() : "",
+			publisher: publisherElement && publisherElement.content ? publisherElement.content.trim() : "",
+			heading: headingElement && headingElement.textContent ? headingElement.textContent.trim() : ""
+		};
+		this.options.infobarContent = await ProcessorHelper.evalTemplate(this.options.infobarTemplate, this.options, null, true);
+	}
+}
+
+// ---------------
+// ProcessorHelper
+// ---------------
+const DATA_URI_PREFIX = "data:";
+const ABOUT_BLANK_URI = "about:blank";
+const EMPTY_DATA_URI = "data:null;base64,";
+const REGEXP_URL_HASH = /(#.+?)$/;
+const SINGLE_FILE_VARIABLE_NAME_PREFIX = "--sf-img-";
+const SINGLE_FILE_VARIABLE_MAX_SIZE = 512 * 1024;
+
+class ProcessorHelper {
+	static async evalTemplate(template = "", options, content, dontReplaceSlash) {
+		const url = util.parseURL(options.saveUrl);
+		template = await evalTemplateVariable(template, "page-title", () => options.title || "No title", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "page-heading", () => options.info.heading || "No heading", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "page-language", () => options.info.lang || "No language", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "page-description", () => options.info.description || "No description", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "page-author", () => options.info.author || "No author", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "page-creator", () => options.info.creator || "No creator", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "page-publisher", () => options.info.publisher || "No publisher", dontReplaceSlash, options.filenameReplacementCharacter);
+		await evalDate(options.saveDate);
+		await evalDate(options.visitDate, "visit-");
+		template = await evalTemplateVariable(template, "url-hash", () => url.hash.substring(1) || "No hash", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-host", () => url.host.replace(/\/$/, "") || "No host", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-hostname", () => url.hostname.replace(/\/$/, "") || "No hostname", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-href", () => decode(url.href) || "No href", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-href-flat", () => decode(url.href) || "No href", false, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-referrer", () => decode(options.referrer) || "No referrer", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-referrer-flat", () => decode(options.referrer) || "No referrer", false, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-password", () => url.password || "No password", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-pathname", () => decode(url.pathname).replace(/^\//, "").replace(/\/$/, "") || "No pathname", dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-pathname-flat", () => decode(url.pathname) || "No pathname", false, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-port", () => url.port || "No port", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-protocol", () => url.protocol || "No protocol", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-search", () => url.search.substring(1) || "No search", dontReplaceSlash, options.filenameReplacementCharacter);
+		const params = url.search.substring(1).split("&").map(parameter => parameter.split("="));
+		for (const [name, value] of params) {
+			template = await evalTemplateVariable(template, "url-search-" + name, () => value || "", dontReplaceSlash, options.filenameReplacementCharacter);
+		}
+		template = template.replace(/{\s*url-search-[^}\s]*\s*}/gi, "");
+		template = await evalTemplateVariable(template, "url-username", () => url.username || "No username", dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "tab-id", () => String(options.tabId || "No tab id"), dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "tab-index", () => String(options.tabIndex || "No tab index"), dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "url-last-segment", () => decode(getLastSegment(url, options.filenameReplacementCharacter)) || "No last segment", dontReplaceSlash, options.filenameReplacementCharacter);
+		if (content) {
+			template = await evalTemplateVariable(template, "digest-sha-256", async () => util.digest("SHA-256", content), dontReplaceSlash, options.filenameReplacementCharacter);
+			template = await evalTemplateVariable(template, "digest-sha-384", async () => util.digest("SHA-384", content), dontReplaceSlash, options.filenameReplacementCharacter);
+			template = await evalTemplateVariable(template, "digest-sha-512", async () => util.digest("SHA-512", content), dontReplaceSlash, options.filenameReplacementCharacter);
+		}
+		const bookmarkFolder = (options.bookmarkFolders && options.bookmarkFolders.join("/")) || "";
+		template = await evalTemplateVariable(template, "bookmark-pathname", () => bookmarkFolder, dontReplaceSlash === undefined ? true : dontReplaceSlash, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "bookmark-pathname-flat", () => bookmarkFolder, false, options.filenameReplacementCharacter);
+		template = await evalTemplateVariable(template, "profile-name", () => options.profileName, dontReplaceSlash, options.filenameReplacementCharacter);
+		return template.trim();
+
+		function decode(value) {
+			try {
+				return decodeURI(value);
+			} catch (error) {
+				return value;
+			}
+		}
+
+		async function evalDate(date, prefix = "") {
+			if (date) {
+				template = await evalTemplateVariable(template, prefix + "datetime-iso", () => date.toISOString(), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "date-iso", () => date.toISOString().split("T")[0], dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "time-iso", () => date.toISOString().split("T")[1].split("Z")[0], dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "date-locale", () => date.toLocaleDateString(), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "time-locale", () => date.toLocaleTimeString(), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "day-locale", () => String(date.getDate()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "month-locale", () => String(date.getMonth() + 1).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "year-locale", () => String(date.getFullYear()), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "datetime-locale", () => date.toLocaleString(), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "datetime-utc", () => date.toUTCString(), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "day-utc", () => String(date.getUTCDate()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "month-utc", () => String(date.getUTCMonth() + 1).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "year-utc", () => String(date.getUTCFullYear()), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "hours-locale", () => String(date.getHours()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "minutes-locale", () => String(date.getMinutes()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "seconds-locale", () => String(date.getSeconds()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "hours-utc", () => String(date.getUTCHours()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "minutes-utc", () => String(date.getUTCMinutes()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "seconds-utc", () => String(date.getUTCSeconds()).padStart(2, "0"), dontReplaceSlash, options.filenameReplacementCharacter);
+				template = await evalTemplateVariable(template, prefix + "time-ms", () => String(date.getTime()), dontReplaceSlash, options.filenameReplacementCharacter);
 			}
 		}
+	}
 
-		static setBackgroundImage(element, url, style) {
-			element.style.setProperty("background-blend-mode", "normal", "important");
-			element.style.setProperty("background-clip", "content-box", "important");
-			element.style.setProperty("background-position", style && style["background-position"] ? style["background-position"] : "center", "important");
-			element.style.setProperty("background-color", style && style["background-color"] ? style["background-color"] : "transparent", "important");
-			element.style.setProperty("background-image", url, "important");
-			element.style.setProperty("background-size", style && style["background-size"] ? style["background-size"] : "100% 100%", "important");
-			element.style.setProperty("background-origin", "content-box", "important");
-			element.style.setProperty("background-repeat", "no-repeat", "important");
-		}
+	static setBackgroundImage(element, url, style) {
+		element.style.setProperty("background-blend-mode", "normal", "important");
+		element.style.setProperty("background-clip", "content-box", "important");
+		element.style.setProperty("background-position", style && style["background-position"] ? style["background-position"] : "center", "important");
+		element.style.setProperty("background-color", style && style["background-color"] ? style["background-color"] : "transparent", "important");
+		element.style.setProperty("background-image", url, "important");
+		element.style.setProperty("background-size", style && style["background-size"] ? style["background-size"] : "100% 100%", "important");
+		element.style.setProperty("background-origin", "content-box", "important");
+		element.style.setProperty("background-repeat", "no-repeat", "important");
+	}
 
-		static processShortcutIcons(doc) {
-			let shortcutIcon = findShortcutIcon(Array.from(doc.querySelectorAll("link[href][rel=\"icon\"], link[href][rel=\"shortcut icon\"]")));
-			if (!shortcutIcon) {
-				shortcutIcon = findShortcutIcon(Array.from(doc.querySelectorAll("link[href][rel*=\"icon\"]")));
-				if (shortcutIcon) {
-					shortcutIcon.rel = "icon";
-				}
-			}
+	static processShortcutIcons(doc) {
+		let shortcutIcon = findShortcutIcon(Array.from(doc.querySelectorAll("link[href][rel=\"icon\"], link[href][rel=\"shortcut icon\"]")));
+		if (!shortcutIcon) {
+			shortcutIcon = findShortcutIcon(Array.from(doc.querySelectorAll("link[href][rel*=\"icon\"]")));
 			if (shortcutIcon) {
-				doc.querySelectorAll("link[href][rel*=\"icon\"]").forEach(linkElement => {
-					if (linkElement != shortcutIcon) {
-						linkElement.remove();
-					}
-				});
+				shortcutIcon.rel = "icon";
 			}
 		}
-
-		static removeSingleLineCssComments(stylesheet) {
-			const removedRules = [];
-			for (let cssRule = stylesheet.children.head; cssRule; cssRule = cssRule.next) {
-				const ruleData = cssRule.data;
-				if (ruleData.type == "Raw" && ruleData.value && ruleData.value.trim().startsWith("//")) {
-					removedRules.push(cssRule);
+		if (shortcutIcon) {
+			doc.querySelectorAll("link[href][rel*=\"icon\"]").forEach(linkElement => {
+				if (linkElement != shortcutIcon) {
+					linkElement.remove();
 				}
+			});
+		}
+	}
+
+	static removeSingleLineCssComments(stylesheet) {
+		const removedRules = [];
+		for (let cssRule = stylesheet.children.head; cssRule; cssRule = cssRule.next) {
+			const ruleData = cssRule.data;
+			if (ruleData.type == "Raw" && ruleData.value && ruleData.value.trim().startsWith("//")) {
+				removedRules.push(cssRule);
 			}
-			removedRules.forEach(cssRule => stylesheet.children.remove(cssRule));
 		}
+		removedRules.forEach(cssRule => stylesheet.children.remove(cssRule));
+	}
 
-		static async resolveImportURLs(stylesheetContent, baseURI, options, workStylesheet, importedStyleSheets = new Set()) {
-			stylesheetContent = ProcessorHelper.resolveStylesheetURLs(stylesheetContent, baseURI, workStylesheet);
-			const imports = getImportFunctions(stylesheetContent);
-			await Promise.all(imports.map(async cssImport => {
-				const match = matchImport(cssImport);
-				if (match) {
-					const regExpCssImport = getRegExp(cssImport);
-					let resourceURL = normalizeURL(match.resourceURL);
-					if (!testIgnoredPath(resourceURL) && testValidPath(resourceURL)) {
-						try {
-							resourceURL = util.resolveURL(match.resourceURL, baseURI);
-						} catch (error) {
-							// ignored
+	static async resolveImportURLs(stylesheetContent, baseURI, options, workStylesheet, importedStyleSheets = new Set()) {
+		stylesheetContent = ProcessorHelper.resolveStylesheetURLs(stylesheetContent, baseURI, workStylesheet);
+		const imports = getImportFunctions(stylesheetContent);
+		await Promise.all(imports.map(async cssImport => {
+			const match = matchImport(cssImport);
+			if (match) {
+				const regExpCssImport = getRegExp(cssImport);
+				let resourceURL = normalizeURL(match.resourceURL);
+				if (!testIgnoredPath(resourceURL) && testValidPath(resourceURL)) {
+					try {
+						resourceURL = util.resolveURL(match.resourceURL, baseURI);
+					} catch (error) {
+						// ignored
+					}
+					if (testValidURL(resourceURL) && !importedStyleSheets.has(resourceURL)) {
+						const content = await getStylesheetContent(resourceURL);
+						resourceURL = content.resourceURL;
+						content.data = getUpdatedResourceContent(resourceURL, content, options);
+						let importedStylesheetContent = removeCssComments(content.data);
+						if (options.compressCSS) {
+							importedStylesheetContent = util.compressCSS(importedStylesheetContent);
 						}
-						if (testValidURL(resourceURL) && !importedStyleSheets.has(resourceURL)) {
-							const content = await getStylesheetContent(resourceURL);
-							resourceURL = content.resourceURL;
-							content.data = getUpdatedResourceContent(resourceURL, content, options);
-							let importedStylesheetContent = removeCssComments(content.data);
-							if (options.compressCSS) {
-								importedStylesheetContent = util.compressCSS(importedStylesheetContent);
-							}
-							importedStylesheetContent = wrapMediaQuery(importedStylesheetContent, match.media);
-							if (stylesheetContent.includes(cssImport)) {
-								const ancestorStyleSheets = new Set(importedStyleSheets);
-								ancestorStyleSheets.add(resourceURL);
-								importedStylesheetContent = await ProcessorHelper.resolveImportURLs(importedStylesheetContent, resourceURL, options, workStylesheet, ancestorStyleSheets);
-								workStylesheet.textContent = importedStylesheetContent;
-								if ((workStylesheet.sheet && workStylesheet.sheet.cssRules.length) || (!workStylesheet.sheet && importedStylesheetContent)) {
-									stylesheetContent = stylesheetContent.replace(regExpCssImport, importedStylesheetContent);
-								} else {
-									stylesheetContent = stylesheetContent.replace(regExpCssImport, "");
-								}
+						importedStylesheetContent = wrapMediaQuery(importedStylesheetContent, match.media);
+						if (stylesheetContent.includes(cssImport)) {
+							const ancestorStyleSheets = new Set(importedStyleSheets);
+							ancestorStyleSheets.add(resourceURL);
+							importedStylesheetContent = await ProcessorHelper.resolveImportURLs(importedStylesheetContent, resourceURL, options, workStylesheet, ancestorStyleSheets);
+							workStylesheet.textContent = importedStylesheetContent;
+							if ((workStylesheet.sheet && workStylesheet.sheet.cssRules.length) || (!workStylesheet.sheet && importedStylesheetContent)) {
+								stylesheetContent = stylesheetContent.replace(regExpCssImport, importedStylesheetContent);
+							} else {
+								stylesheetContent = stylesheetContent.replace(regExpCssImport, "");
 							}
-						} else {
-							stylesheetContent = stylesheetContent.replace(regExpCssImport, "");
 						}
 					} else {
 						stylesheetContent = stylesheetContent.replace(regExpCssImport, "");
 					}
-				}
-			}));
-			return stylesheetContent;
-
-			async function getStylesheetContent(resourceURL) {
-				const content = await util.getContent(resourceURL, {
-					maxResourceSize: options.maxResourceSize,
-					maxResourceSizeEnabled: options.maxResourceSizeEnabled,
-					validateTextContentType: true,
-					frameId: options.frameId,
-					charset: options.charset,
-					resourceReferrer: options.resourceReferrer
-				});
-				if (!matchCharsetEquals(content.data, content.charset || options.charset)) {
-					options = Object.assign({}, options, { charset: getCharset(content.data) });
-					return getStylesheetContent(resourceURL);
 				} else {
-					return content;
+					stylesheetContent = stylesheetContent.replace(regExpCssImport, "");
 				}
 			}
+		}));
+		return stylesheetContent;
+
+		async function getStylesheetContent(resourceURL) {
+			const content = await util.getContent(resourceURL, {
+				maxResourceSize: options.maxResourceSize,
+				maxResourceSizeEnabled: options.maxResourceSizeEnabled,
+				validateTextContentType: true,
+				frameId: options.frameId,
+				charset: options.charset,
+				resourceReferrer: options.resourceReferrer
+			});
+			if (!matchCharsetEquals(content.data, content.charset || options.charset)) {
+				options = Object.assign({}, options, { charset: getCharset(content.data) });
+				return getStylesheetContent(resourceURL);
+			} else {
+				return content;
+			}
 		}
+	}
 
-		static resolveStylesheetURLs(stylesheetContent, baseURI, workStylesheet) {
-			const urlFunctions = getUrlFunctions(stylesheetContent, true);
-			urlFunctions.map(urlFunction => {
-				const originalResourceURL = matchURL(urlFunction);
-				let resourceURL = normalizeURL(originalResourceURL);
-				workStylesheet.textContent = "tmp { content:\"" + resourceURL + "\"}";
-				if (workStylesheet.sheet && workStylesheet.sheet.cssRules) {
-					resourceURL = util.removeQuotes(workStylesheet.sheet.cssRules[0].style.getPropertyValue("content"));
-				}
-				if (!testIgnoredPath(resourceURL)) {
-					if (!resourceURL || testValidPath(resourceURL)) {
-						let resolvedURL;
-						if (!originalResourceURL.startsWith("#")) {
-							try {
-								resolvedURL = util.resolveURL(resourceURL, baseURI);
-							} catch (error) {
-								// ignored
-							}
-						}
-						if (testValidURL(resolvedURL) && resourceURL != resolvedURL && stylesheetContent.includes(urlFunction)) {
-							stylesheetContent = stylesheetContent.replace(getRegExp(urlFunction), originalResourceURL ? urlFunction.replace(originalResourceURL, resolvedURL) : "url(" + resolvedURL + ")");
+	static resolveStylesheetURLs(stylesheetContent, baseURI, workStylesheet) {
+		const urlFunctions = getUrlFunctions(stylesheetContent, true);
+		urlFunctions.map(urlFunction => {
+			const originalResourceURL = matchURL(urlFunction);
+			let resourceURL = normalizeURL(originalResourceURL);
+			workStylesheet.textContent = "tmp { content:\"" + resourceURL + "\"}";
+			if (workStylesheet.sheet && workStylesheet.sheet.cssRules) {
+				resourceURL = util.removeQuotes(workStylesheet.sheet.cssRules[0].style.getPropertyValue("content"));
+			}
+			if (!testIgnoredPath(resourceURL)) {
+				if (!resourceURL || testValidPath(resourceURL)) {
+					let resolvedURL;
+					if (!originalResourceURL.startsWith("#")) {
+						try {
+							resolvedURL = util.resolveURL(resourceURL, baseURI);
+						} catch (error) {
+							// ignored
 						}
+					}
+					if (testValidURL(resolvedURL) && resourceURL != resolvedURL && stylesheetContent.includes(urlFunction)) {
+						stylesheetContent = stylesheetContent.replace(getRegExp(urlFunction), originalResourceURL ? urlFunction.replace(originalResourceURL, resolvedURL) : "url(" + resolvedURL + ")");
+					}
+				} else {
+					let newUrlFunction;
+					if (originalResourceURL) {
+						newUrlFunction = urlFunction.replace(originalResourceURL, EMPTY_DATA_URI);
 					} else {
-						let newUrlFunction;
-						if (originalResourceURL) {
-							newUrlFunction = urlFunction.replace(originalResourceURL, EMPTY_DATA_URI);
-						} else {
-							newUrlFunction = "url(" + EMPTY_DATA_URI + ")";
-						}
-						stylesheetContent = stylesheetContent.replace(getRegExp(urlFunction), newUrlFunction);
+						newUrlFunction = "url(" + EMPTY_DATA_URI + ")";
 					}
+					stylesheetContent = stylesheetContent.replace(getRegExp(urlFunction), newUrlFunction);
 				}
+			}
+		});
+		return stylesheetContent;
+	}
+
+	static async resolveLinkStylesheetURLs(resourceURL, baseURI, options, workStylesheet) {
+		resourceURL = normalizeURL(resourceURL);
+		if (resourceURL && resourceURL != baseURI && resourceURL != ABOUT_BLANK_URI) {
+			const content = await util.getContent(resourceURL, {
+				maxResourceSize: options.maxResourceSize,
+				maxResourceSizeEnabled: options.maxResourceSizeEnabled,
+				charset: options.charset,
+				frameId: options.frameId,
+				resourceReferrer: options.resourceReferrer,
+				validateTextContentType: true
 			});
+			if (!matchCharsetEquals(content.data, content.charset || options.charset)) {
+				options = Object.assign({}, options, { charset: getCharset(content.data) });
+				return ProcessorHelper.resolveLinkStylesheetURLs(resourceURL, baseURI, options, workStylesheet);
+			}
+			resourceURL = content.resourceURL;
+			content.data = getUpdatedResourceContent(content.resourceURL, content, options);
+			let stylesheetContent = removeCssComments(content.data);
+			if (options.compressCSS) {
+				stylesheetContent = util.compressCSS(stylesheetContent);
+			}
+			stylesheetContent = await ProcessorHelper.resolveImportURLs(stylesheetContent, resourceURL, options, workStylesheet);
 			return stylesheetContent;
 		}
+	}
 
-		static async resolveLinkStylesheetURLs(resourceURL, baseURI, options, workStylesheet) {
-			resourceURL = normalizeURL(resourceURL);
-			if (resourceURL && resourceURL != baseURI && resourceURL != ABOUT_BLANK_URI) {
-				const content = await util.getContent(resourceURL, {
-					maxResourceSize: options.maxResourceSize,
-					maxResourceSizeEnabled: options.maxResourceSizeEnabled,
-					charset: options.charset,
-					frameId: options.frameId,
-					resourceReferrer: options.resourceReferrer,
-					validateTextContentType: true
-				});
-				if (!matchCharsetEquals(content.data, content.charset || options.charset)) {
-					options = Object.assign({}, options, { charset: getCharset(content.data) });
-					return ProcessorHelper.resolveLinkStylesheetURLs(resourceURL, baseURI, options, workStylesheet);
-				}
-				resourceURL = content.resourceURL;
-				content.data = getUpdatedResourceContent(content.resourceURL, content, options);
-				let stylesheetContent = removeCssComments(content.data);
-				if (options.compressCSS) {
-					stylesheetContent = util.compressCSS(stylesheetContent);
-				}
-				stylesheetContent = await ProcessorHelper.resolveImportURLs(stylesheetContent, resourceURL, options, workStylesheet);
-				return stylesheetContent;
-			}
-		}
-
-		static async processStylesheet(cssRules, baseURI, options, cssVariables, batchRequest) {
-			const promises = [];
-			const removedRules = [];
-			for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
-				const ruleData = cssRule.data;
-				if (ruleData.type == "Atrule" && ruleData.name == "charset") {
-					removedRules.push(cssRule);
-				} else if (ruleData.block && ruleData.block.children) {
-					if (ruleData.type == "Rule") {
-						promises.push(this.processStyle(ruleData.block.children.toArray(), baseURI, options, cssVariables, batchRequest));
-					} else if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports")) {
-						promises.push(this.processStylesheet(ruleData.block.children, baseURI, options, cssVariables, batchRequest));
-					} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
-						promises.push(processFontFaceRule(ruleData, options.fontURLs));
-					}
-				}
-			}
-			removedRules.forEach(cssRule => cssRules.remove(cssRule));
-			await Promise.all(promises);
-
-			async function processFontFaceRule(ruleData, fontURLs) {
-				await Promise.all(ruleData.block.children.toArray().map(async declaration => {
-					if (declaration.type == "Declaration" && declaration.value.children) {
-						const urlFunctions = getUrlFunctions(getCSSValue(declaration.value), true);
-						await Promise.all(urlFunctions.map(async urlFunction => {
-							const originalResourceURL = matchURL(urlFunction);
-							const resourceURL = normalizeURL(originalResourceURL);
-							if (!testIgnoredPath(resourceURL)) {
-								if (testValidURL(resourceURL)) {
-									let { content } = await batchRequest.addURL(resourceURL, true, "font");
-									let resourceURLs = fontURLs.get(declaration);
-									if (!resourceURLs) {
-										resourceURLs = [];
-										fontURLs.set(declaration, resourceURLs);
-									}
-									resourceURLs.push(resourceURL);
-									replaceURLs(declaration, originalResourceURL, content);
-								}
-							}
-						}));
-					}
-				}));
-
-				function replaceURLs(declaration, oldURL, newURL) {
-					declaration.value.children.forEach(token => {
-						if (token.type == "Url" && util.removeQuotes(getCSSValue(token.value)) == oldURL) {
-							token.value.value = newURL;
-						}
-					});
+	static async processStylesheet(cssRules, baseURI, options, cssVariables, batchRequest) {
+		const promises = [];
+		const removedRules = [];
+		for (let cssRule = cssRules.head; cssRule; cssRule = cssRule.next) {
+			const ruleData = cssRule.data;
+			if (ruleData.type == "Atrule" && ruleData.name == "charset") {
+				removedRules.push(cssRule);
+			} else if (ruleData.block && ruleData.block.children) {
+				if (ruleData.type == "Rule") {
+					promises.push(this.processStyle(ruleData.block.children.toArray(), baseURI, options, cssVariables, batchRequest));
+				} else if (ruleData.type == "Atrule" && (ruleData.name == "media" || ruleData.name == "supports")) {
+					promises.push(this.processStylesheet(ruleData.block.children, baseURI, options, cssVariables, batchRequest));
+				} else if (ruleData.type == "Atrule" && ruleData.name == "font-face") {
+					promises.push(processFontFaceRule(ruleData, options.fontURLs));
 				}
 			}
 		}
+		removedRules.forEach(cssRule => cssRules.remove(cssRule));
+		await Promise.all(promises);
 
-		static async processStyle(declarations, baseURI, options, cssVariables, batchRequest) {
-			await Promise.all(declarations.map(async declaration => {
-				if (declaration.value && !declaration.value.children && declaration.value.type == "Raw") {
-					try {
-						declaration.value = cssTree.parse(declaration.value.value, { context: "value" });
-					} catch (error) {
-						// ignored
-					}
-				}
+		async function processFontFaceRule(ruleData, fontURLs) {
+			await Promise.all(ruleData.block.children.toArray().map(async declaration => {
 				if (declaration.type == "Declaration" && declaration.value.children) {
-					const urlFunctions = getUrlFunctions(getCSSValue(declaration.value));
+					const urlFunctions = getUrlFunctions(getCSSValue(declaration.value), true);
 					await Promise.all(urlFunctions.map(async urlFunction => {
 						const originalResourceURL = matchURL(urlFunction);
 						const resourceURL = normalizeURL(originalResourceURL);
 						if (!testIgnoredPath(resourceURL)) {
 							if (testValidURL(resourceURL)) {
-								let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL, true, "image", true);
-								let variableDefined;
-								const tokens = [];
-								findURLToken(originalResourceURL, declaration.value.children, (token, parent, rootFunction) => {
-									if (!originalResourceURL.startsWith("#")) {
-										if (duplicate && options.groupDuplicateImages && rootFunction && util.getContentSize(content) < SINGLE_FILE_VARIABLE_MAX_SIZE) {
-											const value = cssTree.parse("var(" + SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource + ")", { context: "value" }).children.head;
-											tokens.push({ parent, token, value });
-											variableDefined = true;
-										} else {
-											token.data.value.value = content;
-										}
-									}
-								});
-								if (variableDefined) {
-									cssVariables.set(indexResource, content);
-									tokens.forEach(({ parent, token, value }) => parent.replace(token, value));
+								let { content } = await batchRequest.addURL(resourceURL, true, "font");
+								let resourceURLs = fontURLs.get(declaration);
+								if (!resourceURLs) {
+									resourceURLs = [];
+									fontURLs.set(declaration, resourceURLs);
 								}
+								resourceURLs.push(resourceURL);
+								replaceURLs(declaration, originalResourceURL, content);
 							}
 						}
 					}));
 				}
 			}));
 
-			function findURLToken(url, children, callback, depth = 0) {
-				for (let token = children.head; token; token = token.next) {
-					if (token.data.children) {
-						findURLToken(url, token.data.children, callback, depth + 1);
+			function replaceURLs(declaration, oldURL, newURL) {
+				declaration.value.children.forEach(token => {
+					if (token.type == "Url" && util.removeQuotes(getCSSValue(token.value)) == oldURL) {
+						token.value.value = newURL;
 					}
-					if (token.data.type == "Url" && util.removeQuotes(getCSSValue(token.data.value)) == url) {
-						callback(token, children, depth == 0);
-					}
-				}
+				});
 			}
 		}
+	}
 
-		static async processAttribute(resourceElements, attributeName, baseURI, options, cssVariables, styles, batchRequest, processDuplicates, removeElementIfMissing) {
-			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
-				let resourceURL = resourceElement.getAttribute(attributeName);
-				if (resourceURL != null) {
-					resourceURL = normalizeURL(resourceURL);
-					let originURL = resourceElement.dataset.singleFileOriginURL;
-					delete resourceElement.dataset.singleFileOriginURL;
+	static async processStyle(declarations, baseURI, options, cssVariables, batchRequest) {
+		await Promise.all(declarations.map(async declaration => {
+			if (declaration.value && !declaration.value.children && declaration.value.type == "Raw") {
+				try {
+					declaration.value = cssTree.parse(declaration.value.value, { context: "value" });
+				} catch (error) {
+					// ignored
+				}
+			}
+			if (declaration.type == "Declaration" && declaration.value.children) {
+				const urlFunctions = getUrlFunctions(getCSSValue(declaration.value));
+				await Promise.all(urlFunctions.map(async urlFunction => {
+					const originalResourceURL = matchURL(urlFunction);
+					const resourceURL = normalizeURL(originalResourceURL);
 					if (!testIgnoredPath(resourceURL)) {
-						resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
-						if (testValidPath(resourceURL)) {
-							try {
-								resourceURL = util.resolveURL(resourceURL, baseURI);
-							} catch (error) {
-								// ignored
+						if (testValidURL(resourceURL)) {
+							let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL, true, "image", true);
+							let variableDefined;
+							const tokens = [];
+							findURLToken(originalResourceURL, declaration.value.children, (token, parent, rootFunction) => {
+								if (!originalResourceURL.startsWith("#")) {
+									if (duplicate && options.groupDuplicateImages && rootFunction && util.getContentSize(content) < SINGLE_FILE_VARIABLE_MAX_SIZE) {
+										const value = cssTree.parse("var(" + SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource + ")", { context: "value" }).children.head;
+										tokens.push({ parent, token, value });
+										variableDefined = true;
+									} else {
+										token.data.value.value = content;
+									}
+								}
+							});
+							if (variableDefined) {
+								cssVariables.set(indexResource, content);
+								tokens.forEach(({ parent, token, value }) => parent.replace(token, value));
 							}
-							if (testValidURL(resourceURL)) {
-								let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL, true, "image", resourceElement.tagName == "IMG" && attributeName == "src");
-								if (originURL) {
-									if (content == EMPTY_DATA_URI) {
-										try {
-											originURL = util.resolveURL(originURL, baseURI);
-										} catch (error) {
-											// ignored
-										}
-										try {
-											resourceURL = originURL;
-											content = (await util.getContent(resourceURL, {
-												asBinary: true,
-												expectedType: "image",
-												maxResourceSize: options.maxResourceSize,
-												maxResourceSizeEnabled: options.maxResourceSizeEnabled,
-												frameId: options.windowId,
-												resourceReferrer: options.resourceReferrer
-											})).data;
-										} catch (error) {
-											// ignored
-										}
+						}
+					}
+				}));
+			}
+		}));
+
+		function findURLToken(url, children, callback, depth = 0) {
+			for (let token = children.head; token; token = token.next) {
+				if (token.data.children) {
+					findURLToken(url, token.data.children, callback, depth + 1);
+				}
+				if (token.data.type == "Url" && util.removeQuotes(getCSSValue(token.data.value)) == url) {
+					callback(token, children, depth == 0);
+				}
+			}
+		}
+	}
+
+	static async processAttribute(resourceElements, attributeName, baseURI, options, cssVariables, styles, batchRequest, processDuplicates, removeElementIfMissing) {
+		await Promise.all(Array.from(resourceElements).map(async resourceElement => {
+			let resourceURL = resourceElement.getAttribute(attributeName);
+			if (resourceURL != null) {
+				resourceURL = normalizeURL(resourceURL);
+				let originURL = resourceElement.dataset.singleFileOriginURL;
+				delete resourceElement.dataset.singleFileOriginURL;
+				if (!testIgnoredPath(resourceURL)) {
+					resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
+					if (testValidPath(resourceURL)) {
+						try {
+							resourceURL = util.resolveURL(resourceURL, baseURI);
+						} catch (error) {
+							// ignored
+						}
+						if (testValidURL(resourceURL)) {
+							let { content, indexResource, duplicate } = await batchRequest.addURL(resourceURL, true, "image", resourceElement.tagName == "IMG" && attributeName == "src");
+							if (originURL) {
+								if (content == EMPTY_DATA_URI) {
+									try {
+										originURL = util.resolveURL(originURL, baseURI);
+									} catch (error) {
+										// ignored
+									}
+									try {
+										resourceURL = originURL;
+										content = (await util.getContent(resourceURL, {
+											asBinary: true,
+											expectedType: "image",
+											maxResourceSize: options.maxResourceSize,
+											maxResourceSizeEnabled: options.maxResourceSizeEnabled,
+											frameId: options.windowId,
+											resourceReferrer: options.resourceReferrer
+										})).data;
+									} catch (error) {
+										// ignored
 									}
 								}
-								if (removeElementIfMissing && content == EMPTY_DATA_URI) {
-									resourceElement.remove();
-								} else {
-									const forbiddenPrefixFound = PREFIXES_FORBIDDEN_DATA_URI.filter(prefixDataURI => content.startsWith(prefixDataURI)).length;
-									if (!forbiddenPrefixFound) {
-										const isSVG = content.startsWith(PREFIX_DATA_URI_IMAGE_SVG);
-										if (processDuplicates && duplicate && options.groupDuplicateImages && !isSVG && util.getContentSize(content) < SINGLE_FILE_VARIABLE_MAX_SIZE) {
-											if (ProcessorHelper.replaceImageSource(resourceElement, SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource, options)) {
-												cssVariables.set(indexResource, content);
-												const declarationList = cssTree.parse(resourceElement.getAttribute("style"), { context: "declarationList" });
-												styles.set(resourceElement, declarationList);
-											} else {
-												resourceElement.setAttribute(attributeName, content);
-											}
+							}
+							if (removeElementIfMissing && content == EMPTY_DATA_URI) {
+								resourceElement.remove();
+							} else {
+								const forbiddenPrefixFound = PREFIXES_FORBIDDEN_DATA_URI.filter(prefixDataURI => content.startsWith(prefixDataURI)).length;
+								if (!forbiddenPrefixFound) {
+									const isSVG = content.startsWith(PREFIX_DATA_URI_IMAGE_SVG);
+									if (processDuplicates && duplicate && options.groupDuplicateImages && !isSVG && util.getContentSize(content) < SINGLE_FILE_VARIABLE_MAX_SIZE) {
+										if (ProcessorHelper.replaceImageSource(resourceElement, SINGLE_FILE_VARIABLE_NAME_PREFIX + indexResource, options)) {
+											cssVariables.set(indexResource, content);
+											const declarationList = cssTree.parse(resourceElement.getAttribute("style"), { context: "declarationList" });
+											styles.set(resourceElement, declarationList);
 										} else {
 											resourceElement.setAttribute(attributeName, content);
 										}
+									} else {
+										resourceElement.setAttribute(attributeName, content);
 									}
 								}
 							}
 						}
 					}
 				}
-			}));
-		}
+			}
+		}));
+	}
 
-		static async processXLinks(resourceElements, baseURI, options, batchRequest) {
-			let attributeName = "xlink:href";
-			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
-				let originalResourceURL = resourceElement.getAttribute(attributeName);
-				if (originalResourceURL == null) {
-					attributeName = "href";
-					originalResourceURL = resourceElement.getAttribute(attributeName);
+	static async processXLinks(resourceElements, baseURI, options, batchRequest) {
+		let attributeName = "xlink:href";
+		await Promise.all(Array.from(resourceElements).map(async resourceElement => {
+			let originalResourceURL = resourceElement.getAttribute(attributeName);
+			if (originalResourceURL == null) {
+				attributeName = "href";
+				originalResourceURL = resourceElement.getAttribute(attributeName);
+			}
+			let resourceURL = normalizeURL(originalResourceURL);
+			if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
+				resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
+				try {
+					resourceURL = util.resolveURL(resourceURL, baseURI);
+				} catch (error) {
+					// ignored
 				}
-				let resourceURL = normalizeURL(originalResourceURL);
-				if (testValidPath(resourceURL) && !testIgnoredPath(resourceURL)) {
-					resourceElement.setAttribute(attributeName, EMPTY_IMAGE);
-					try {
-						resourceURL = util.resolveURL(resourceURL, baseURI);
-					} catch (error) {
-						// ignored
-					}
-					if (testValidURL(resourceURL)) {
-						const { content } = await batchRequest.addURL(resourceURL);
-						const hashMatch = originalResourceURL.match(REGEXP_URL_HASH);
-						if (hashMatch && hashMatch[0]) {
-							let symbolElement;
-							try {
-								symbolElement = util.parseSVGContent(content).querySelector(hashMatch[0]);
-							} catch (error) {
-								// ignored
-							}
-							if (symbolElement) {
-								resourceElement.setAttribute(attributeName, hashMatch[0]);
-								resourceElement.parentElement.insertBefore(symbolElement, resourceElement.parentElement.firstChild);
-							}
-						} else {
-							resourceElement.setAttribute(attributeName, PREFIX_DATA_URI_IMAGE_SVG + "," + content);
+				if (testValidURL(resourceURL)) {
+					const { content } = await batchRequest.addURL(resourceURL);
+					const hashMatch = originalResourceURL.match(REGEXP_URL_HASH);
+					if (hashMatch && hashMatch[0]) {
+						let symbolElement;
+						try {
+							symbolElement = util.parseSVGContent(content).querySelector(hashMatch[0]);
+						} catch (error) {
+							// ignored
+						}
+						if (symbolElement) {
+							resourceElement.setAttribute(attributeName, hashMatch[0]);
+							resourceElement.parentElement.insertBefore(symbolElement, resourceElement.parentElement.firstChild);
 						}
+					} else {
+						resourceElement.setAttribute(attributeName, PREFIX_DATA_URI_IMAGE_SVG + "," + content);
 					}
-				} else if (resourceURL == options.url) {
-					resourceElement.setAttribute(attributeName, originalResourceURL.substring(resourceURL.length));
 				}
-			}));
-		}
+			} else if (resourceURL == options.url) {
+				resourceElement.setAttribute(attributeName, originalResourceURL.substring(resourceURL.length));
+			}
+		}));
+	}
 
-		static async processSrcset(resourceElements, attributeName, baseURI, batchRequest) {
-			await Promise.all(Array.from(resourceElements).map(async resourceElement => {
-				const srcset = util.parseSrcset(resourceElement.getAttribute(attributeName));
-				const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
-					let resourceURL = normalizeURL(srcsetValue.url);
-					if (!testIgnoredPath(resourceURL)) {
-						if (testValidPath(resourceURL)) {
-							try {
-								resourceURL = util.resolveURL(resourceURL, baseURI);
-							} catch (error) {
-								// ignored
-							}
-							if (testValidURL(resourceURL)) {
-								const { content } = await batchRequest.addURL(resourceURL, true, "image");
-								const forbiddenPrefixFound = PREFIXES_FORBIDDEN_DATA_URI.filter(prefixDataURI => content.startsWith(prefixDataURI)).length;
-								if (forbiddenPrefixFound) {
-									return "";
-								}
-								return content + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
-							} else {
+	static async processSrcset(resourceElements, attributeName, baseURI, batchRequest) {
+		await Promise.all(Array.from(resourceElements).map(async resourceElement => {
+			const srcset = util.parseSrcset(resourceElement.getAttribute(attributeName));
+			const srcsetValues = await Promise.all(srcset.map(async srcsetValue => {
+				let resourceURL = normalizeURL(srcsetValue.url);
+				if (!testIgnoredPath(resourceURL)) {
+					if (testValidPath(resourceURL)) {
+						try {
+							resourceURL = util.resolveURL(resourceURL, baseURI);
+						} catch (error) {
+							// ignored
+						}
+						if (testValidURL(resourceURL)) {
+							const { content } = await batchRequest.addURL(resourceURL, true, "image");
+							const forbiddenPrefixFound = PREFIXES_FORBIDDEN_DATA_URI.filter(prefixDataURI => content.startsWith(prefixDataURI)).length;
+							if (forbiddenPrefixFound) {
 								return "";
 							}
+							return content + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
 						} else {
 							return "";
 						}
 					} else {
-						return resourceURL + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
+						return "";
 					}
-				}));
-				resourceElement.setAttribute(attributeName, srcsetValues.join(", "));
+				} else {
+					return resourceURL + (srcsetValue.w ? " " + srcsetValue.w + "w" : srcsetValue.d ? " " + srcsetValue.d + "x" : "");
+				}
 			}));
-		}
+			resourceElement.setAttribute(attributeName, srcsetValues.join(", "));
+		}));
+	}
 
-		static replaceImageSource(imgElement, variableName, options) {
-			const attributeValue = imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME);
-			if (attributeValue) {
-				const imageData = options.images[Number(imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME))];
-				if (imageData && imageData.replaceable) {
-					imgElement.setAttribute("src", `${PREFIX_DATA_URI_IMAGE_SVG},<svg xmlns="http://www.w3.org/2000/svg" width="${imageData.size.pxWidth}" height="${imageData.size.pxHeight}"><rect fill-opacity="0"/></svg>`);
-					const backgroundStyle = {};
-					const backgroundSize = (imageData.objectFit == "content" || imageData.objectFit == "cover") && imageData.objectFit;
-					if (backgroundSize) {
-						backgroundStyle["background-size"] = imageData.objectFit;
-					}
-					if (imageData.objectPosition) {
-						backgroundStyle["background-position"] = imageData.objectPosition;
-					}
-					if (imageData.backgroundColor) {
-						backgroundStyle["background-color"] = imageData.backgroundColor;
-					}
-					ProcessorHelper.setBackgroundImage(imgElement, "var(" + variableName + ")", backgroundStyle);
-					imgElement.removeAttribute(util.IMAGE_ATTRIBUTE_NAME);
-					return true;
+	static replaceImageSource(imgElement, variableName, options) {
+		const attributeValue = imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME);
+		if (attributeValue) {
+			const imageData = options.images[Number(imgElement.getAttribute(util.IMAGE_ATTRIBUTE_NAME))];
+			if (imageData && imageData.replaceable) {
+				imgElement.setAttribute("src", `${PREFIX_DATA_URI_IMAGE_SVG},<svg xmlns="http://www.w3.org/2000/svg" width="${imageData.size.pxWidth}" height="${imageData.size.pxHeight}"><rect fill-opacity="0"/></svg>`);
+				const backgroundStyle = {};
+				const backgroundSize = (imageData.objectFit == "content" || imageData.objectFit == "cover") && imageData.objectFit;
+				if (backgroundSize) {
+					backgroundStyle["background-size"] = imageData.objectFit;
+				}
+				if (imageData.objectPosition) {
+					backgroundStyle["background-position"] = imageData.objectPosition;
 				}
+				if (imageData.backgroundColor) {
+					backgroundStyle["background-color"] = imageData.backgroundColor;
+				}
+				ProcessorHelper.setBackgroundImage(imgElement, "var(" + variableName + ")", backgroundStyle);
+				imgElement.removeAttribute(util.IMAGE_ATTRIBUTE_NAME);
+				return true;
 			}
 		}
 	}
-
-	// ----
-	// Util
-	// ----
-	const BLOB_URI_PREFIX = "blob:";
-	const HTTP_URI_PREFIX = /^https?:\/\//;
-	const FILE_URI_PREFIX = /^file:\/\//;
-	const EMPTY_URL = /^https?:\/\/+\s*$/;
-	const NOT_EMPTY_URL = /^(https?:\/\/|file:\/\/|blob:).+/;
-	const REGEXP_URL_FN = /(url\s*\(\s*'(.*?)'\s*\))|(url\s*\(\s*"(.*?)"\s*\))|(url\s*\(\s*(.*?)\s*\))/gi;
-	const REGEXP_URL_SIMPLE_QUOTES_FN = /^url\s*\(\s*'(.*?)'\s*\)$/i;
-	const REGEXP_URL_DOUBLE_QUOTES_FN = /^url\s*\(\s*"(.*?)"\s*\)$/i;
-	const REGEXP_URL_NO_QUOTES_FN = /^url\s*\(\s*(.*?)\s*\)$/i;
-	const REGEXP_IMPORT_FN = /(@import\s*url\s*\(\s*'(.*?)'\s*\)\s*(.*?)(;|$|}))|(@import\s*url\s*\(\s*"(.*?)"\s*\)\s*(.*?)(;|$|}))|(@import\s*url\s*\(\s*(.*?)\s*\)\s*(.*?)(;|$|}))|(@import\s*'(.*?)'\s*(.*?)(;|$|}))|(@import\s*"(.*?)"\s*(.*?)(;|$|}))|(@import\s*(.*?)\s*(.*?)(;|$|}))/gi;
-	const REGEXP_IMPORT_URL_SIMPLE_QUOTES_FN = /@import\s*url\s*\(\s*'(.*?)'\s*\)\s*(.*?)(;|$|})/i;
-	const REGEXP_IMPORT_URL_DOUBLE_QUOTES_FN = /@import\s*url\s*\(\s*"(.*?)"\s*\)\s*(.*?)(;|$|})/i;
-	const REGEXP_IMPORT_URL_NO_QUOTES_FN = /@import\s*url\s*\(\s*(.*?)\s*\)\s*(.*?)(;|$|})/i;
-	const REGEXP_IMPORT_SIMPLE_QUOTES_FN = /@import\s*'(.*?)'\s*(.*?)(;|$|})/i;
-	const REGEXP_IMPORT_DOUBLE_QUOTES_FN = /@import\s*"(.*?)"\s*(.*?)(;|$|})/i;
-	const REGEXP_IMPORT_NO_QUOTES_FN = /@import\s*(.*?)\s*(.*?)(;|$|})/i;
-	const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
-
-	function getUpdatedResourceContent(resourceURL, content, options) {
-		if (options.rootDocument && options.updatedResources[resourceURL]) {
-			options.updatedResources[resourceURL].retrieved = true;
-			return options.updatedResources[resourceURL].content;
-		} else {
-			return content.data || "";
-		}
+}
+
+// ----
+// Util
+// ----
+const BLOB_URI_PREFIX = "blob:";
+const HTTP_URI_PREFIX = /^https?:\/\//;
+const FILE_URI_PREFIX = /^file:\/\//;
+const EMPTY_URL = /^https?:\/\/+\s*$/;
+const NOT_EMPTY_URL = /^(https?:\/\/|file:\/\/|blob:).+/;
+const REGEXP_URL_FN = /(url\s*\(\s*'(.*?)'\s*\))|(url\s*\(\s*"(.*?)"\s*\))|(url\s*\(\s*(.*?)\s*\))/gi;
+const REGEXP_URL_SIMPLE_QUOTES_FN = /^url\s*\(\s*'(.*?)'\s*\)$/i;
+const REGEXP_URL_DOUBLE_QUOTES_FN = /^url\s*\(\s*"(.*?)"\s*\)$/i;
+const REGEXP_URL_NO_QUOTES_FN = /^url\s*\(\s*(.*?)\s*\)$/i;
+const REGEXP_IMPORT_FN = /(@import\s*url\s*\(\s*'(.*?)'\s*\)\s*(.*?)(;|$|}))|(@import\s*url\s*\(\s*"(.*?)"\s*\)\s*(.*?)(;|$|}))|(@import\s*url\s*\(\s*(.*?)\s*\)\s*(.*?)(;|$|}))|(@import\s*'(.*?)'\s*(.*?)(;|$|}))|(@import\s*"(.*?)"\s*(.*?)(;|$|}))|(@import\s*(.*?)\s*(.*?)(;|$|}))/gi;
+const REGEXP_IMPORT_URL_SIMPLE_QUOTES_FN = /@import\s*url\s*\(\s*'(.*?)'\s*\)\s*(.*?)(;|$|})/i;
+const REGEXP_IMPORT_URL_DOUBLE_QUOTES_FN = /@import\s*url\s*\(\s*"(.*?)"\s*\)\s*(.*?)(;|$|})/i;
+const REGEXP_IMPORT_URL_NO_QUOTES_FN = /@import\s*url\s*\(\s*(.*?)\s*\)\s*(.*?)(;|$|})/i;
+const REGEXP_IMPORT_SIMPLE_QUOTES_FN = /@import\s*'(.*?)'\s*(.*?)(;|$|})/i;
+const REGEXP_IMPORT_DOUBLE_QUOTES_FN = /@import\s*"(.*?)"\s*(.*?)(;|$|})/i;
+const REGEXP_IMPORT_NO_QUOTES_FN = /@import\s*(.*?)\s*(.*?)(;|$|})/i;
+const REGEXP_ESCAPE = /([{}()^$&.*?/+|[\\\\]|\]|-)/g;
+
+function getUpdatedResourceContent(resourceURL, content, options) {
+	if (options.rootDocument && options.updatedResources[resourceURL]) {
+		options.updatedResources[resourceURL].retrieved = true;
+		return options.updatedResources[resourceURL].content;
+	} else {
+		return content.data || "";
 	}
+}
 
-	function normalizeURL(url) {
-		if (!url || url.startsWith(DATA_URI_PREFIX)) {
-			return url;
-		} else {
-			return url.split("#")[0];
-		}
+function normalizeURL(url) {
+	if (!url || url.startsWith(DATA_URI_PREFIX)) {
+		return url;
+	} else {
+		return url.split("#")[0];
 	}
-
-	function getCSSValue(value) {
-		let result = "";
-		try {
-			result = cssTree.generate(value);
-		} catch (error) {
-			// ignored
-		}
-		return result;
+}
+
+function getCSSValue(value) {
+	let result = "";
+	try {
+		result = cssTree.generate(value);
+	} catch (error) {
+		// ignored
+	}
+	return result;
+}
+
+function matchCharsetEquals(stylesheetContent, charset = "utf-8") {
+	const stylesheetCharset = getCharset(stylesheetContent);
+	if (stylesheetCharset) {
+		return stylesheetCharset == charset.toLowerCase();
+	} else {
+		return true;
 	}
+}
 
-	function matchCharsetEquals(stylesheetContent, charset = "utf-8") {
-		const stylesheetCharset = getCharset(stylesheetContent);
-		if (stylesheetCharset) {
-			return stylesheetCharset == charset.toLowerCase();
-		} else {
-			return true;
-		}
+function getCharset(stylesheetContent) {
+	const match = stylesheetContent.match(/^@charset\s+"([^"]*)";/i);
+	if (match && match[1]) {
+		return match[1].toLowerCase().trim();
 	}
+}
 
-	function getCharset(stylesheetContent) {
-		const match = stylesheetContent.match(/^@charset\s+"([^"]*)";/i);
-		if (match && match[1]) {
-			return match[1].toLowerCase().trim();
+function getOnEventAttributeNames(doc) {
+	const element = doc.createElement("div");
+	const attributeNames = [];
+	for (const propertyName in element) {
+		if (propertyName.startsWith("on")) {
+			attributeNames.push(propertyName);
 		}
 	}
-
-	function getOnEventAttributeNames(doc) {
-		const element = doc.createElement("div");
-		const attributeNames = [];
-		for (const propertyName in element) {
-			if (propertyName.startsWith("on")) {
-				attributeNames.push(propertyName);
+	attributeNames.push("onunload");
+	return attributeNames;
+}
+
+async function evalTemplateVariable(template, variableName, valueGetter, dontReplaceSlash, replacementCharacter) {
+	let maxLength;
+	if (template) {
+		const regExpVariable = "{\\s*" + variableName.replace(/\W|_/g, "[$&]") + "\\s*}";
+		let replaceRegExp = new RegExp(regExpVariable + "\\[\\d+\\]", "g");
+		if (template.match(replaceRegExp)) {
+			const matchedLength = template.match(replaceRegExp)[0];
+			maxLength = Number(matchedLength.match(/\[(\d+)\]$/)[1]);
+			if (isNaN(maxLength) || maxLength <= 0) {
+				maxLength = null;
 			}
+		} else {
+			replaceRegExp = new RegExp(regExpVariable, "g");
 		}
-		attributeNames.push("onunload");
-		return attributeNames;
-	}
-
-	async function evalTemplateVariable(template, variableName, valueGetter, dontReplaceSlash, replacementCharacter) {
-		let maxLength;
-		if (template) {
-			const regExpVariable = "{\\s*" + variableName.replace(/\W|_/g, "[$&]") + "\\s*}";
-			let replaceRegExp = new RegExp(regExpVariable + "\\[\\d+\\]", "g");
-			if (template.match(replaceRegExp)) {
-				const matchedLength = template.match(replaceRegExp)[0];
-				maxLength = Number(matchedLength.match(/\[(\d+)\]$/)[1]);
-				if (isNaN(maxLength) || maxLength <= 0) {
-					maxLength = null;
-				}
-			} else {
-				replaceRegExp = new RegExp(regExpVariable, "g");
+		if (template.match(replaceRegExp)) {
+			let value = await valueGetter();
+			if (!dontReplaceSlash) {
+				value = value.replace(/\/+/g, replacementCharacter);
 			}
-			if (template.match(replaceRegExp)) {
-				let value = await valueGetter();
-				if (!dontReplaceSlash) {
-					value = value.replace(/\/+/g, replacementCharacter);
-				}
-				if (maxLength) {
-					value = await util.truncateText(value, maxLength);
-				}
-				return template.replace(replaceRegExp, value);
+			if (maxLength) {
+				value = await util.truncateText(value, maxLength);
 			}
+			return template.replace(replaceRegExp, value);
 		}
-		return template;
 	}
-
-	function getLastSegment(url, replacementCharacter) {
-		let lastSegmentMatch = url.pathname.match(/\/([^/]+)$/), lastSegment = lastSegmentMatch && lastSegmentMatch[0];
-		if (!lastSegment) {
-			lastSegmentMatch = url.href.match(/([^/]+)\/?$/);
-			lastSegment = lastSegmentMatch && lastSegmentMatch[0];
-		}
-		if (!lastSegment) {
-			lastSegmentMatch = lastSegment.match(/(.*)\.[^.]+$/);
-			lastSegment = lastSegmentMatch && lastSegmentMatch[0];
-		}
-		if (!lastSegment) {
-			lastSegment = url.hostname.replace(/\/+/g, replacementCharacter).replace(/\/$/, "");
-		}
-		lastSegmentMatch = lastSegment.match(/(.*)\.[^.]+$/);
-		if (lastSegmentMatch && lastSegmentMatch[1]) {
-			lastSegment = lastSegmentMatch[1];
-		}
-		lastSegment = lastSegment.replace(/\/$/, "").replace(/^\//, "");
-		return lastSegment;
+	return template;
+}
+
+function getLastSegment(url, replacementCharacter) {
+	let lastSegmentMatch = url.pathname.match(/\/([^/]+)$/), lastSegment = lastSegmentMatch && lastSegmentMatch[0];
+	if (!lastSegment) {
+		lastSegmentMatch = url.href.match(/([^/]+)\/?$/);
+		lastSegment = lastSegmentMatch && lastSegmentMatch[0];
 	}
-
-	function getRegExp(string) {
-		return new RegExp(string.replace(REGEXP_ESCAPE, "\\$1"), "gi");
+	if (!lastSegment) {
+		lastSegmentMatch = lastSegment.match(/(.*)\.[^.]+$/);
+		lastSegment = lastSegmentMatch && lastSegmentMatch[0];
 	}
-
-	function getUrlFunctions(stylesheetContent, unique) {
-		const result = stylesheetContent.match(REGEXP_URL_FN) || [];
-		if (unique) {
-			return [...new Set(result)];
-		} else {
-			return result;
-		}
+	if (!lastSegment) {
+		lastSegment = url.hostname.replace(/\/+/g, replacementCharacter).replace(/\/$/, "");
 	}
-
-	function getImportFunctions(stylesheetContent) {
-		return stylesheetContent.match(REGEXP_IMPORT_FN) || [];
+	lastSegmentMatch = lastSegment.match(/(.*)\.[^.]+$/);
+	if (lastSegmentMatch && lastSegmentMatch[1]) {
+		lastSegment = lastSegmentMatch[1];
 	}
-
-	function findShortcutIcon(shortcutIcons) {
-		shortcutIcons = shortcutIcons.filter(linkElement => linkElement.href != EMPTY_IMAGE);
-		shortcutIcons.sort((linkElement1, linkElement2) => (parseInt(linkElement2.sizes, 10) || 16) - (parseInt(linkElement1.sizes, 10) || 16));
-		return shortcutIcons[0];
+	lastSegment = lastSegment.replace(/\/$/, "").replace(/^\//, "");
+	return lastSegment;
+}
+
+function getRegExp(string) {
+	return new RegExp(string.replace(REGEXP_ESCAPE, "\\$1"), "gi");
+}
+
+function getUrlFunctions(stylesheetContent, unique) {
+	const result = stylesheetContent.match(REGEXP_URL_FN) || [];
+	if (unique) {
+		return [...new Set(result)];
+	} else {
+		return result;
 	}
-
-	function matchURL(stylesheetContent) {
-		const match = stylesheetContent.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
-			stylesheetContent.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
-			stylesheetContent.match(REGEXP_URL_NO_QUOTES_FN);
-		return match && match[1];
+}
+
+function getImportFunctions(stylesheetContent) {
+	return stylesheetContent.match(REGEXP_IMPORT_FN) || [];
+}
+
+function findShortcutIcon(shortcutIcons) {
+	shortcutIcons = shortcutIcons.filter(linkElement => linkElement.href != EMPTY_IMAGE);
+	shortcutIcons.sort((linkElement1, linkElement2) => (parseInt(linkElement2.sizes, 10) || 16) - (parseInt(linkElement1.sizes, 10) || 16));
+	return shortcutIcons[0];
+}
+
+function matchURL(stylesheetContent) {
+	const match = stylesheetContent.match(REGEXP_URL_SIMPLE_QUOTES_FN) ||
+		stylesheetContent.match(REGEXP_URL_DOUBLE_QUOTES_FN) ||
+		stylesheetContent.match(REGEXP_URL_NO_QUOTES_FN);
+	return match && match[1];
+}
+
+function testIgnoredPath(resourceURL) {
+	return resourceURL && (resourceURL.startsWith(DATA_URI_PREFIX) || resourceURL == ABOUT_BLANK_URI);
+}
+
+function testValidPath(resourceURL) {
+	return resourceURL && !resourceURL.match(EMPTY_URL);
+}
+
+function testValidURL(resourceURL) {
+	return testValidPath(resourceURL) && (resourceURL.match(HTTP_URI_PREFIX) || resourceURL.match(FILE_URI_PREFIX) || resourceURL.startsWith(BLOB_URI_PREFIX)) && resourceURL.match(NOT_EMPTY_URL);
+}
+
+function matchImport(stylesheetContent) {
+	const match = stylesheetContent.match(REGEXP_IMPORT_URL_SIMPLE_QUOTES_FN) ||
+		stylesheetContent.match(REGEXP_IMPORT_URL_DOUBLE_QUOTES_FN) ||
+		stylesheetContent.match(REGEXP_IMPORT_URL_NO_QUOTES_FN) ||
+		stylesheetContent.match(REGEXP_IMPORT_SIMPLE_QUOTES_FN) ||
+		stylesheetContent.match(REGEXP_IMPORT_DOUBLE_QUOTES_FN) ||
+		stylesheetContent.match(REGEXP_IMPORT_NO_QUOTES_FN);
+	if (match) {
+		const [, resourceURL, media] = match;
+		return { resourceURL, media };
 	}
-
-	function testIgnoredPath(resourceURL) {
-		return resourceURL && (resourceURL.startsWith(DATA_URI_PREFIX) || resourceURL == ABOUT_BLANK_URI);
+}
+
+function removeCssComments(stylesheetContent) {
+	try {
+		return stylesheetContent.replace(/\/\*(.|\s)*?\*\//g, "");
+	} catch (error) {
+		let start, end;
+		do {
+			start = stylesheetContent.indexOf("/*");
+			end = stylesheetContent.indexOf("*/", start + 2);
+			if (start != -1 && end != -1) {
+				stylesheetContent = stylesheetContent.substring(0, start) + stylesheetContent.substr(end + 2);
+			}
+		} while (start != -1 && end != -1);
+		return stylesheetContent;
 	}
+}
 
-	function testValidPath(resourceURL) {
-		return resourceURL && !resourceURL.match(EMPTY_URL);
+function wrapMediaQuery(stylesheetContent, mediaQuery) {
+	if (mediaQuery) {
+		return "@media " + mediaQuery + "{ " + stylesheetContent + " }";
+	} else {
+		return stylesheetContent;
 	}
-
-	function testValidURL(resourceURL) {
-		return testValidPath(resourceURL) && (resourceURL.match(HTTP_URI_PREFIX) || resourceURL.match(FILE_URI_PREFIX) || resourceURL.startsWith(BLOB_URI_PREFIX)) && resourceURL.match(NOT_EMPTY_URL);
+}
+
+function log(...args) {
+	console.log("S-File <core>   ", ...args); // eslint-disable-line no-console
+}
+
+// -----
+// Stats
+// -----
+const STATS_DEFAULT_VALUES = {
+	discarded: {
+		"HTML bytes": 0,
+		"hidden elements": 0,
+		"HTML imports": 0,
+		scripts: 0,
+		objects: 0,
+		"audio sources": 0,
+		"video sources": 0,
+		frames: 0,
+		"CSS rules": 0,
+		canvas: 0,
+		stylesheets: 0,
+		resources: 0,
+		medias: 0
+	},
+	processed: {
+		"HTML bytes": 0,
+		"hidden elements": 0,
+		"HTML imports": 0,
+		scripts: 0,
+		objects: 0,
+		"audio sources": 0,
+		"video sources": 0,
+		frames: 0,
+		"CSS rules": 0,
+		canvas: 0,
+		stylesheets: 0,
+		resources: 0,
+		medias: 0
 	}
+};
 
-	function matchImport(stylesheetContent) {
-		const match = stylesheetContent.match(REGEXP_IMPORT_URL_SIMPLE_QUOTES_FN) ||
-			stylesheetContent.match(REGEXP_IMPORT_URL_DOUBLE_QUOTES_FN) ||
-			stylesheetContent.match(REGEXP_IMPORT_URL_NO_QUOTES_FN) ||
-			stylesheetContent.match(REGEXP_IMPORT_SIMPLE_QUOTES_FN) ||
-			stylesheetContent.match(REGEXP_IMPORT_DOUBLE_QUOTES_FN) ||
-			stylesheetContent.match(REGEXP_IMPORT_NO_QUOTES_FN);
-		if (match) {
-			const [, resourceURL, media] = match;
-			return { resourceURL, media };
+class Stats {
+	constructor(options) {
+		this.options = options;
+		if (options.displayStats) {
+			this.data = JSON.parse(JSON.stringify(STATS_DEFAULT_VALUES));
 		}
 	}
-
-	function removeCssComments(stylesheetContent) {
-		try {
-			return stylesheetContent.replace(/\/\*(.|\s)*?\*\//g, "");
-		} catch (error) {
-			let start, end;
-			do {
-				start = stylesheetContent.indexOf("/*");
-				end = stylesheetContent.indexOf("*/", start + 2);
-				if (start != -1 && end != -1) {
-					stylesheetContent = stylesheetContent.substring(0, start) + stylesheetContent.substr(end + 2);
-				}
-			} while (start != -1 && end != -1);
-			return stylesheetContent;
+	set(type, subType, value) {
+		if (this.options.displayStats) {
+			this.data[type][subType] = value;
 		}
 	}
-
-	function wrapMediaQuery(stylesheetContent, mediaQuery) {
-		if (mediaQuery) {
-			return "@media " + mediaQuery + "{ " + stylesheetContent + " }";
-		} else {
-			return stylesheetContent;
+	add(type, subType, value) {
+		if (this.options.displayStats) {
+			this.data[type][subType] += value;
 		}
 	}
-
-	function log(...args) {
-		console.log("S-File <core>   ", ...args); // eslint-disable-line no-console
-	}
-
-	// -----
-	// Stats
-	// -----
-	const STATS_DEFAULT_VALUES = {
-		discarded: {
-			"HTML bytes": 0,
-			"hidden elements": 0,
-			"HTML imports": 0,
-			scripts: 0,
-			objects: 0,
-			"audio sources": 0,
-			"video sources": 0,
-			frames: 0,
-			"CSS rules": 0,
-			canvas: 0,
-			stylesheets: 0,
-			resources: 0,
-			medias: 0
-		},
-		processed: {
-			"HTML bytes": 0,
-			"hidden elements": 0,
-			"HTML imports": 0,
-			scripts: 0,
-			objects: 0,
-			"audio sources": 0,
-			"video sources": 0,
-			frames: 0,
-			"CSS rules": 0,
-			canvas: 0,
-			stylesheets: 0,
-			resources: 0,
-			medias: 0
-		}
-	};
-
-	class Stats {
-		constructor(options) {
-			this.options = options;
-			if (options.displayStats) {
-				this.data = JSON.parse(JSON.stringify(STATS_DEFAULT_VALUES));
-			}
-		}
-		set(type, subType, value) {
-			if (this.options.displayStats) {
-				this.data[type][subType] = value;
-			}
-		}
-		add(type, subType, value) {
-			if (this.options.displayStats) {
-				this.data[type][subType] += value;
-			}
-		}
-		addAll(pageData) {
-			if (this.options.displayStats) {
-				Object.keys(this.data.discarded).forEach(key => this.add("discarded", key, pageData.stats.discarded[key] || 0));
-				Object.keys(this.data.processed).forEach(key => this.add("processed", key, pageData.stats.processed[key] || 0));
-			}
+	addAll(pageData) {
+		if (this.options.displayStats) {
+			Object.keys(this.data.discarded).forEach(key => this.add("discarded", key, pageData.stats.discarded[key] || 0));
+			Object.keys(this.data.processed).forEach(key => this.add("processed", key, pageData.stats.processed[key] || 0));
 		}
 	}
+}
 
-	return { getClass };
-
-})(typeof globalThis == "object" ? globalThis : window);
+export {
+	getClass
+};

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

@@ -0,0 +1 @@
+import "./processors/frame-tree/content/content-frame-tree.js";

+ 415 - 416
lib/single-file/single-file-helper.js

@@ -21,489 +21,488 @@
  *   Source.
  */
 
-/* global window, globalThis, CustomEvent */
+/* global globalThis, CustomEvent */
 
-this.singlefile.lib.helper = this.singlefile.lib.helper || (globalThis => {
+import * as cssUnescape from "./vendor/css-unescape.js";
+import * as hooksFrames from "./processors/hooks/content/content-hooks-frames";
 
-	const singlefile = this.singlefile;
+const ON_BEFORE_CAPTURE_EVENT_NAME = "single-file-on-before-capture";
+const ON_AFTER_CAPTURE_EVENT_NAME = "single-file-on-after-capture";
+const REMOVED_CONTENT_ATTRIBUTE_NAME = "data-single-file-removed-content";
+const HIDDEN_CONTENT_ATTRIBUTE_NAME = "data-single-file-hidden-content";
+const KEPT_CONTENT_ATTRIBUTE_NAME = "data-single-file-kept-content";
+const HIDDEN_FRAME_ATTRIBUTE_NAME = "data-single-file-hidden-frame";
+const PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME = "data-single-file-preserved-space-element";
+const SHADOW_ROOT_ATTRIBUTE_NAME = "data-single-file-shadow-root-element";
+const WIN_ID_ATTRIBUTE_NAME = "data-single-file-win-id";
+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 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";
+const DISABLED_NOSCRIPT_ATTRIBUTE_NAME = "data-single-file-disabled-noscript";
+const SELECTED_CONTENT_ATTRIBUTE_NAME = "data-single-file-selected-content";
+const ASYNC_SCRIPT_ATTRIBUTE_NAME = "data-single-file-async-script";
+const FLOW_ELEMENTS_SELECTOR = "*:not(base):not(link):not(meta):not(noscript):not(script):not(style):not(template):not(title)";
+const KEPT_TAG_NAMES = ["NOSCRIPT", "DISABLED-NOSCRIPT", "META", "LINK", "STYLE", "TITLE", "TEMPLATE", "SOURCE", "OBJECT", "SCRIPT", "HEAD"];
+const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
+const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
+const FONT_WEIGHTS = {
+	regular: "400",
+	normal: "400",
+	bold: "700",
+	bolder: "700",
+	lighter: "100"
+};
+const COMMENT_HEADER = "Page saved with SingleFile";
+const COMMENT_HEADER_LEGACY = "Archive processed by SingleFile";
+const SINGLE_FILE_UI_ELEMENT_CLASS = "single-file-ui-element";
+const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
+const dispatchEvent = event => globalThis.dispatchEvent(event);
 
-	const ON_BEFORE_CAPTURE_EVENT_NAME = "single-file-on-before-capture";
-	const ON_AFTER_CAPTURE_EVENT_NAME = "single-file-on-after-capture";
-	const REMOVED_CONTENT_ATTRIBUTE_NAME = "data-single-file-removed-content";
-	const HIDDEN_CONTENT_ATTRIBUTE_NAME = "data-single-file-hidden-content";
-	const KEPT_CONTENT_ATTRIBUTE_NAME = "data-single-file-kept-content";
-	const HIDDEN_FRAME_ATTRIBUTE_NAME = "data-single-file-hidden-frame";
-	const PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME = "data-single-file-preserved-space-element";
-	const SHADOW_ROOT_ATTRIBUTE_NAME = "data-single-file-shadow-root-element";
-	const WIN_ID_ATTRIBUTE_NAME = "data-single-file-win-id";
-	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 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";
-	const DISABLED_NOSCRIPT_ATTRIBUTE_NAME = "data-single-file-disabled-noscript";
-	const SELECTED_CONTENT_ATTRIBUTE_NAME = "data-single-file-selected-content";
-	const ASYNC_SCRIPT_ATTRIBUTE_NAME = "data-single-file-async-script";
-	const FLOW_ELEMENTS_SELECTOR = "*:not(base):not(link):not(meta):not(noscript):not(script):not(style):not(template):not(title)";
-	const KEPT_TAG_NAMES = ["NOSCRIPT", "DISABLED-NOSCRIPT", "META", "LINK", "STYLE", "TITLE", "TEMPLATE", "SOURCE", "OBJECT", "SCRIPT", "HEAD"];
-	const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
-	const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
-	const FONT_WEIGHTS = {
-		regular: "400",
-		normal: "400",
-		bold: "700",
-		bolder: "700",
-		lighter: "100"
-	};
-	const COMMENT_HEADER = "Page saved with SingleFile";
-	const COMMENT_HEADER_LEGACY = "Archive processed by SingleFile";
-	const SINGLE_FILE_UI_ELEMENT_CLASS = "single-file-ui-element";
-	const addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
-	const dispatchEvent = event => globalThis.dispatchEvent(event);
+let waitForUserScript = {};
 
-	addEventListener("single-file-user-script-init", () => singlefile.lib.helper.waitForUserScript = async eventPrefixName => {
-		const event = new CustomEvent(eventPrefixName + "-request", { cancelable: true });
-		const promiseResponse = new Promise(resolve => addEventListener(eventPrefixName + "-response", resolve));
-		dispatchEvent(event);
-		if (event.defaultPrevented) {
-			await promiseResponse;
-		}
+addEventListener("single-file-user-script-init", () => waitForUserScript.callback = async eventPrefixName => {
+	const event = new CustomEvent(eventPrefixName + "-request", { cancelable: true });
+	const promiseResponse = new Promise(resolve => addEventListener(eventPrefixName + "-response", resolve));
+	dispatchEvent(event);
+	if (event.defaultPrevented) {
+		await promiseResponse;
+	}
+});
+
+export {
+	waitForUserScript,
+	initDoc,
+	preProcessDoc,
+	postProcessDoc,
+	serialize,
+	removeQuotes,
+	flatten,
+	getFontWeight,
+	normalizeFontFamily,
+	getShadowRoot,
+	ON_BEFORE_CAPTURE_EVENT_NAME,
+	ON_AFTER_CAPTURE_EVENT_NAME,
+	WIN_ID_ATTRIBUTE_NAME,
+	PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME,
+	REMOVED_CONTENT_ATTRIBUTE_NAME,
+	HIDDEN_CONTENT_ATTRIBUTE_NAME,
+	HIDDEN_FRAME_ATTRIBUTE_NAME,
+	IMAGE_ATTRIBUTE_NAME,
+	POSTER_ATTRIBUTE_NAME,
+	CANVAS_ATTRIBUTE_NAME,
+	INPUT_VALUE_ATTRIBUTE_NAME,
+	SHADOW_ROOT_ATTRIBUTE_NAME,
+	HTML_IMPORT_ATTRIBUTE_NAME,
+	LAZY_SRC_ATTRIBUTE_NAME,
+	STYLESHEET_ATTRIBUTE_NAME,
+	SELECTED_CONTENT_ATTRIBUTE_NAME,
+	ASYNC_SCRIPT_ATTRIBUTE_NAME,
+	COMMENT_HEADER,
+	COMMENT_HEADER_LEGACY,
+	SINGLE_FILE_UI_ELEMENT_CLASS
+};
+
+function initDoc(doc) {
+	doc.querySelectorAll("meta[http-equiv=refresh]").forEach(element => {
+		element.removeAttribute("http-equiv");
+		element.setAttribute("disabled-http-equiv", "refresh");
 	});
-	return {
-		initDoc,
-		preProcessDoc,
-		postProcessDoc,
-		serialize,
-		removeQuotes,
-		flatten,
-		getFontWeight,
-		normalizeFontFamily,
-		getShadowRoot,
-		ON_BEFORE_CAPTURE_EVENT_NAME,
-		ON_AFTER_CAPTURE_EVENT_NAME,
-		WIN_ID_ATTRIBUTE_NAME,
-		PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME,
-		REMOVED_CONTENT_ATTRIBUTE_NAME,
-		HIDDEN_CONTENT_ATTRIBUTE_NAME,
-		HIDDEN_FRAME_ATTRIBUTE_NAME,
-		IMAGE_ATTRIBUTE_NAME,
-		POSTER_ATTRIBUTE_NAME,
-		CANVAS_ATTRIBUTE_NAME,
-		INPUT_VALUE_ATTRIBUTE_NAME,
-		SHADOW_ROOT_ATTRIBUTE_NAME,
-		HTML_IMPORT_ATTRIBUTE_NAME,
-		LAZY_SRC_ATTRIBUTE_NAME,
-		STYLESHEET_ATTRIBUTE_NAME,
-		SELECTED_CONTENT_ATTRIBUTE_NAME,
-		ASYNC_SCRIPT_ATTRIBUTE_NAME,
-		COMMENT_HEADER,
-		COMMENT_HEADER_LEGACY,
-		SINGLE_FILE_UI_ELEMENT_CLASS
-	};
+}
 
-	function initDoc(doc) {
-		doc.querySelectorAll("meta[http-equiv=refresh]").forEach(element => {
-			element.removeAttribute("http-equiv");
-			element.setAttribute("disabled-http-equiv", "refresh");
-		});
+function preProcessDoc(doc, win, options) {
+	doc.querySelectorAll("noscript:not([" + DISABLED_NOSCRIPT_ATTRIBUTE_NAME + "])").forEach(element => {
+		element.setAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME, element.textContent);
+		element.textContent = "";
+	});
+	initDoc(doc);
+	if (doc.head) {
+		doc.head.querySelectorAll(FLOW_ELEMENTS_SELECTOR).forEach(element => element.hidden = true);
 	}
-
-	function preProcessDoc(doc, win, options) {
-		doc.querySelectorAll("noscript:not([" + DISABLED_NOSCRIPT_ATTRIBUTE_NAME + "])").forEach(element => {
-			element.setAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME, element.textContent);
-			element.textContent = "";
-		});
-		initDoc(doc);
-		if (doc.head) {
-			doc.head.querySelectorAll(FLOW_ELEMENTS_SELECTOR).forEach(element => element.hidden = true);
+	doc.querySelectorAll("svg foreignObject").forEach(element => {
+		const flowElements = element.querySelectorAll("html > head > " + FLOW_ELEMENTS_SELECTOR + ", html > body > " + FLOW_ELEMENTS_SELECTOR);
+		if (flowElements.length) {
+			Array.from(element.childNodes).forEach(node => node.remove());
+			flowElements.forEach(flowElement => element.appendChild(flowElement));
 		}
-		doc.querySelectorAll("svg foreignObject").forEach(element => {
-			const flowElements = element.querySelectorAll("html > head > " + FLOW_ELEMENTS_SELECTOR + ", html > body > " + FLOW_ELEMENTS_SELECTOR);
-			if (flowElements.length) {
-				Array.from(element.childNodes).forEach(node => node.remove());
-				flowElements.forEach(flowElement => element.appendChild(flowElement));
-			}
-		});
-		let elementsInfo;
-		if (win && doc.documentElement) {
-			elementsInfo = getElementsInfo(win, doc, doc.documentElement, options);
-		} else {
-			elementsInfo = {
-				canvases: [],
-				images: [],
-				posters: [],
-				usedFonts: [],
-				shadowRoots: [],
-				imports: [],
-				markedElements: []
-			};
-		}
-		return {
-			canvases: elementsInfo.canvases,
-			fonts: getFontsData(doc),
-			stylesheets: getStylesheetsData(doc),
-			images: elementsInfo.images,
-			posters: elementsInfo.posters,
-			usedFonts: Array.from(elementsInfo.usedFonts.values()),
-			shadowRoots: elementsInfo.shadowRoots,
-			imports: elementsInfo.imports,
-			referrer: doc.referrer,
-			markedElements: elementsInfo.markedElements
+	});
+	let elementsInfo;
+	if (win && doc.documentElement) {
+		elementsInfo = getElementsInfo(win, doc, doc.documentElement, options);
+	} else {
+		elementsInfo = {
+			canvases: [],
+			images: [],
+			posters: [],
+			usedFonts: [],
+			shadowRoots: [],
+			imports: [],
+			markedElements: []
 		};
 	}
+	return {
+		canvases: elementsInfo.canvases,
+		fonts: getFontsData(doc),
+		stylesheets: getStylesheetsData(doc),
+		images: elementsInfo.images,
+		posters: elementsInfo.posters,
+		usedFonts: Array.from(elementsInfo.usedFonts.values()),
+		shadowRoots: elementsInfo.shadowRoots,
+		imports: elementsInfo.imports,
+		referrer: doc.referrer,
+		markedElements: elementsInfo.markedElements
+	};
+}
 
-	function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map(), canvases: [], images: [], posters: [], shadowRoots: [], imports: [], markedElements: [] }, ascendantHidden) {
-		const elements = Array.from(element.childNodes).filter(node => (node instanceof win.HTMLElement) || (node instanceof win.SVGElement));
-		elements.forEach(element => {
-			let elementHidden, elementKept, computedStyle;
-			if (!options.autoSaveExternalSave && (options.removeHiddenElements || options.removeUnusedFonts || options.compressHTML)) {
-				computedStyle = win.getComputedStyle(element);
-				if (element instanceof win.HTMLElement) {
-					if (options.removeHiddenElements) {
-						elementKept = ((ascendantHidden || element.closest("html > head")) && KEPT_TAG_NAMES.includes(element.tagName)) || element.closest("details");
-						if (!elementKept) {
-							elementHidden = ascendantHidden || testHiddenElement(element, computedStyle);
-							if (elementHidden) {
-								element.setAttribute(HIDDEN_CONTENT_ATTRIBUTE_NAME, "");
-								data.markedElements.push(element);
-							}
-						}
-					}
-				}
-				if (!elementHidden) {
-					if (options.compressHTML && computedStyle) {
-						const whiteSpace = computedStyle.getPropertyValue("white-space");
-						if (whiteSpace && whiteSpace.startsWith("pre")) {
-							element.setAttribute(PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME, "");
+function getElementsInfo(win, doc, element, options, data = { usedFonts: new Map(), canvases: [], images: [], posters: [], shadowRoots: [], imports: [], markedElements: [] }, ascendantHidden) {
+	const elements = Array.from(element.childNodes).filter(node => (node instanceof win.HTMLElement) || (node instanceof win.SVGElement));
+	elements.forEach(element => {
+		let elementHidden, elementKept, computedStyle;
+		if (!options.autoSaveExternalSave && (options.removeHiddenElements || options.removeUnusedFonts || options.compressHTML)) {
+			computedStyle = win.getComputedStyle(element);
+			if (element instanceof win.HTMLElement) {
+				if (options.removeHiddenElements) {
+					elementKept = ((ascendantHidden || element.closest("html > head")) && KEPT_TAG_NAMES.includes(element.tagName)) || element.closest("details");
+					if (!elementKept) {
+						elementHidden = ascendantHidden || testHiddenElement(element, computedStyle);
+						if (elementHidden) {
+							element.setAttribute(HIDDEN_CONTENT_ATTRIBUTE_NAME, "");
 							data.markedElements.push(element);
 						}
 					}
-					if (options.removeUnusedFonts) {
-						getUsedFont(computedStyle, options, data.usedFonts);
-						getUsedFont(win.getComputedStyle(element, ":first-letter"), options, data.usedFonts);
-						getUsedFont(win.getComputedStyle(element, ":before"), options, data.usedFonts);
-						getUsedFont(win.getComputedStyle(element, ":after"), options, data.usedFonts);
-					}
-				}
-			}
-			getResourcesInfo(win, doc, element, options, data, elementHidden, computedStyle);
-			const shadowRoot = getShadowRoot(element);
-			if (shadowRoot && !element.classList.contains(SINGLE_FILE_UI_ELEMENT_CLASS)) {
-				const shadowRootInfo = {};
-				element.setAttribute(SHADOW_ROOT_ATTRIBUTE_NAME, data.shadowRoots.length);
-				data.markedElements.push(element);
-				data.shadowRoots.push(shadowRootInfo);
-				getElementsInfo(win, doc, shadowRoot, options, data, elementHidden);
-				shadowRootInfo.content = shadowRoot.innerHTML;
-				shadowRootInfo.delegatesFocus = shadowRoot.delegatesFocus;
-				shadowRootInfo.mode = shadowRoot.mode;
-				if (shadowRoot.adoptedStyleSheets && shadowRoot.adoptedStyleSheets.length) {
-					shadowRootInfo.adoptedStyleSheets = Array.from(shadowRoot.adoptedStyleSheets).map(stylesheet => Array.from(stylesheet.cssRules).map(cssRule => cssRule.cssText).join("\n"));
 				}
 			}
-			getElementsInfo(win, doc, element, options, data, elementHidden);
-			if (!options.autoSaveExternalSave && options.removeHiddenElements && ascendantHidden) {
-				if (elementKept || element.getAttribute(KEPT_CONTENT_ATTRIBUTE_NAME) == "") {
-					if (element.parentElement) {
-						element.parentElement.setAttribute(KEPT_CONTENT_ATTRIBUTE_NAME, "");
-						data.markedElements.push(element.parentElement);
+			if (!elementHidden) {
+				if (options.compressHTML && computedStyle) {
+					const whiteSpace = computedStyle.getPropertyValue("white-space");
+					if (whiteSpace && whiteSpace.startsWith("pre")) {
+						element.setAttribute(PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME, "");
+						data.markedElements.push(element);
 					}
-				} else if (elementHidden) {
-					element.setAttribute(REMOVED_CONTENT_ATTRIBUTE_NAME, "");
-					data.markedElements.push(element);
 				}
-			}
-		});
-		return data;
-	}
-
-	function getResourcesInfo(win, doc, element, options, data, elementHidden, computedStyle) {
-		if (element.tagName == "CANVAS") {
-			try {
-				const size = getSize(win, element, computedStyle);
-				data.canvases.push({ dataURI: element.toDataURL("image/png", ""), width: size.width, height: size.height });
-				element.setAttribute(CANVAS_ATTRIBUTE_NAME, data.canvases.length - 1);
-				data.markedElements.push(element);
-			} catch (error) {
-				// ignored
+				if (options.removeUnusedFonts) {
+					getUsedFont(computedStyle, options, data.usedFonts);
+					getUsedFont(win.getComputedStyle(element, ":first-letter"), options, data.usedFonts);
+					getUsedFont(win.getComputedStyle(element, ":before"), options, data.usedFonts);
+					getUsedFont(win.getComputedStyle(element, ":after"), options, data.usedFonts);
+				}
 			}
 		}
-		if (element.tagName == "IMG") {
-			const imageData = {
-				currentSrc: elementHidden ?
-					"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" :
-					(options.loadDeferredImages && element.getAttribute(LAZY_SRC_ATTRIBUTE_NAME)) || element.currentSrc
-			};
-			data.images.push(imageData);
-			element.setAttribute(IMAGE_ATTRIBUTE_NAME, data.images.length - 1);
+		getResourcesInfo(win, doc, element, options, data, elementHidden, computedStyle);
+		const shadowRoot = !(element instanceof win.SVGElement) && getShadowRoot(element);
+		if (shadowRoot && !element.classList.contains(SINGLE_FILE_UI_ELEMENT_CLASS)) {
+			const shadowRootInfo = {};
+			element.setAttribute(SHADOW_ROOT_ATTRIBUTE_NAME, data.shadowRoots.length);
 			data.markedElements.push(element);
-			element.removeAttribute(LAZY_SRC_ATTRIBUTE_NAME);
-			computedStyle = computedStyle || win.getComputedStyle(element);
-			if (computedStyle) {
-				imageData.size = getSize(win, element, computedStyle);
-				const boxShadow = computedStyle.getPropertyValue("box-shadow");
-				const backgroundImage = computedStyle.getPropertyValue("background-image");
-				if ((!boxShadow || boxShadow == "none") &&
-					(!backgroundImage || backgroundImage == "none") &&
-					(imageData.size.pxWidth > 1 || imageData.size.pxHeight > 1)) {
-					imageData.replaceable = true;
-					imageData.backgroundColor = computedStyle.getPropertyValue("background-color");
-					imageData.objectFit = computedStyle.getPropertyValue("object-fit");
-					imageData.boxSizing = computedStyle.getPropertyValue("box-sizing");
-					imageData.objectPosition = computedStyle.getPropertyValue("object-position");
-				}
+			data.shadowRoots.push(shadowRootInfo);
+			getElementsInfo(win, doc, shadowRoot, options, data, elementHidden);
+			shadowRootInfo.content = shadowRoot.innerHTML;
+			shadowRootInfo.delegatesFocus = shadowRoot.delegatesFocus;
+			shadowRootInfo.mode = shadowRoot.mode;
+			if (shadowRoot.adoptedStyleSheets && shadowRoot.adoptedStyleSheets.length) {
+				shadowRootInfo.adoptedStyleSheets = Array.from(shadowRoot.adoptedStyleSheets).map(stylesheet => Array.from(stylesheet.cssRules).map(cssRule => cssRule.cssText).join("\n"));
 			}
 		}
-		if (element.tagName == "VIDEO") {
-			if (!element.poster) {
-				const canvasElement = doc.createElement("canvas");
-				const context = canvasElement.getContext("2d");
-				canvasElement.width = element.clientWidth;
-				canvasElement.height = element.clientHeight;
-				try {
-					context.drawImage(element, 0, 0, canvasElement.width, canvasElement.height);
-					data.posters.push(canvasElement.toDataURL("image/png", ""));
-					element.setAttribute(POSTER_ATTRIBUTE_NAME, data.posters.length - 1);
-					data.markedElements.push(element);
-				} catch (error) {
-					// ignored
+		getElementsInfo(win, doc, element, options, data, elementHidden);
+		if (!options.autoSaveExternalSave && options.removeHiddenElements && ascendantHidden) {
+			if (elementKept || element.getAttribute(KEPT_CONTENT_ATTRIBUTE_NAME) == "") {
+				if (element.parentElement) {
+					element.parentElement.setAttribute(KEPT_CONTENT_ATTRIBUTE_NAME, "");
+					data.markedElements.push(element.parentElement);
 				}
-			}
-		}
-		if (element.tagName == "IFRAME") {
-			if (elementHidden && options.removeHiddenElements) {
-				element.setAttribute(HIDDEN_FRAME_ATTRIBUTE_NAME, "");
+			} else if (elementHidden) {
+				element.setAttribute(REMOVED_CONTENT_ATTRIBUTE_NAME, "");
 				data.markedElements.push(element);
 			}
 		}
-		if (element.tagName == "LINK") {
-			if (element.import && element.import.documentElement) {
-				data.imports.push({ content: serialize(element.import) });
-				element.setAttribute(HTML_IMPORT_ATTRIBUTE_NAME, data.imports.length - 1);
-				data.markedElements.push(element);
-			}
+	});
+	return data;
+}
+
+function getResourcesInfo(win, doc, element, options, data, elementHidden, computedStyle) {
+	if (element.tagName == "CANVAS") {
+		try {
+			const size = getSize(win, element, computedStyle);
+			data.canvases.push({ dataURI: element.toDataURL("image/png", ""), width: size.width, height: size.height });
+			element.setAttribute(CANVAS_ATTRIBUTE_NAME, data.canvases.length - 1);
+			data.markedElements.push(element);
+		} catch (error) {
+			// ignored
 		}
-		if (element.tagName == "INPUT") {
-			if (element.type != "password") {
-				element.setAttribute(INPUT_VALUE_ATTRIBUTE_NAME, element.value);
-				data.markedElements.push(element);
+	}
+	if (element.tagName == "IMG") {
+		const imageData = {
+			currentSrc: elementHidden ?
+				"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" :
+				(options.loadDeferredImages && element.getAttribute(LAZY_SRC_ATTRIBUTE_NAME)) || element.currentSrc
+		};
+		data.images.push(imageData);
+		element.setAttribute(IMAGE_ATTRIBUTE_NAME, data.images.length - 1);
+		data.markedElements.push(element);
+		element.removeAttribute(LAZY_SRC_ATTRIBUTE_NAME);
+		computedStyle = computedStyle || win.getComputedStyle(element);
+		if (computedStyle) {
+			imageData.size = getSize(win, element, computedStyle);
+			const boxShadow = computedStyle.getPropertyValue("box-shadow");
+			const backgroundImage = computedStyle.getPropertyValue("background-image");
+			if ((!boxShadow || boxShadow == "none") &&
+				(!backgroundImage || backgroundImage == "none") &&
+				(imageData.size.pxWidth > 1 || imageData.size.pxHeight > 1)) {
+				imageData.replaceable = true;
+				imageData.backgroundColor = computedStyle.getPropertyValue("background-color");
+				imageData.objectFit = computedStyle.getPropertyValue("object-fit");
+				imageData.boxSizing = computedStyle.getPropertyValue("box-sizing");
+				imageData.objectPosition = computedStyle.getPropertyValue("object-position");
 			}
-			if (element.type == "radio" || element.type == "checkbox") {
-				element.setAttribute(INPUT_VALUE_ATTRIBUTE_NAME, element.checked);
+		}
+	}
+	if (element.tagName == "VIDEO") {
+		if (!element.poster) {
+			const canvasElement = doc.createElement("canvas");
+			const context = canvasElement.getContext("2d");
+			canvasElement.width = element.clientWidth;
+			canvasElement.height = element.clientHeight;
+			try {
+				context.drawImage(element, 0, 0, canvasElement.width, canvasElement.height);
+				data.posters.push(canvasElement.toDataURL("image/png", ""));
+				element.setAttribute(POSTER_ATTRIBUTE_NAME, data.posters.length - 1);
 				data.markedElements.push(element);
+			} catch (error) {
+				// ignored
 			}
 		}
-		if (element.tagName == "TEXTAREA") {
+	}
+	if (element.tagName == "IFRAME") {
+		if (elementHidden && options.removeHiddenElements) {
+			element.setAttribute(HIDDEN_FRAME_ATTRIBUTE_NAME, "");
+			data.markedElements.push(element);
+		}
+	}
+	if (element.tagName == "LINK") {
+		if (element.import && element.import.documentElement) {
+			data.imports.push({ content: serialize(element.import) });
+			element.setAttribute(HTML_IMPORT_ATTRIBUTE_NAME, data.imports.length - 1);
+			data.markedElements.push(element);
+		}
+	}
+	if (element.tagName == "INPUT") {
+		if (element.type != "password") {
 			element.setAttribute(INPUT_VALUE_ATTRIBUTE_NAME, element.value);
 			data.markedElements.push(element);
 		}
-		if (element.tagName == "SELECT") {
-			element.querySelectorAll("option").forEach(option => {
-				if (option.selected) {
-					option.setAttribute(INPUT_VALUE_ATTRIBUTE_NAME, "");
-					data.markedElements.push(option);
-				}
-			});
+		if (element.type == "radio" || element.type == "checkbox") {
+			element.setAttribute(INPUT_VALUE_ATTRIBUTE_NAME, element.checked);
+			data.markedElements.push(element);
 		}
-		if (element.tagName == "SCRIPT") {
-			if (element.async && element.getAttribute("async") != "" && element.getAttribute("async") != "async") {
-				element.setAttribute(ASYNC_SCRIPT_ATTRIBUTE_NAME, "");
-				data.markedElements.push(element);
+	}
+	if (element.tagName == "TEXTAREA") {
+		element.setAttribute(INPUT_VALUE_ATTRIBUTE_NAME, element.value);
+		data.markedElements.push(element);
+	}
+	if (element.tagName == "SELECT") {
+		element.querySelectorAll("option").forEach(option => {
+			if (option.selected) {
+				option.setAttribute(INPUT_VALUE_ATTRIBUTE_NAME, "");
+				data.markedElements.push(option);
 			}
-			element.textContent = element.textContent.replace(/<\/script>/gi, "<\\/script>");
-		}
+		});
 	}
-
-	function getUsedFont(computedStyle, options, usedFonts) {
-		if (computedStyle) {
-			const fontStyle = computedStyle.getPropertyValue("font-style") || "normal";
-			computedStyle.getPropertyValue("font-family").split(",").forEach(fontFamilyName => {
-				fontFamilyName = normalizeFontFamily(fontFamilyName);
-				if (!options.loadedFonts || options.loadedFonts.find(font => normalizeFontFamily(font.family) == fontFamilyName && font.style == fontStyle)) {
-					const fontWeight = getFontWeight(computedStyle.getPropertyValue("font-weight"));
-					const fontVariant = computedStyle.getPropertyValue("font-variant") || "normal";
-					const value = [fontFamilyName, fontWeight, fontStyle, fontVariant];
-					usedFonts.set(JSON.stringify(value), [fontFamilyName, fontWeight, fontStyle, fontVariant]);
-				}
-			});
+	if (element.tagName == "SCRIPT") {
+		if (element.async && element.getAttribute("async") != "" && element.getAttribute("async") != "async") {
+			element.setAttribute(ASYNC_SCRIPT_ATTRIBUTE_NAME, "");
+			data.markedElements.push(element);
 		}
+		element.textContent = element.textContent.replace(/<\/script>/gi, "<\\/script>");
 	}
+}
 
-	function getShadowRoot(element) {
-		const chrome = globalThis.chrome;
-		if (element.openOrClosedShadowRoot) {
-			return element.openOrClosedShadowRoot;
-		} else if (chrome && chrome.dom && chrome.dom.openOrClosedShadowRoot) {
-			try {
-				return chrome.dom.openOrClosedShadowRoot(element);
-			} catch (error) {
-				return element.shadowRoot;
+function getUsedFont(computedStyle, options, usedFonts) {
+	if (computedStyle) {
+		const fontStyle = computedStyle.getPropertyValue("font-style") || "normal";
+		computedStyle.getPropertyValue("font-family").split(",").forEach(fontFamilyName => {
+			fontFamilyName = normalizeFontFamily(fontFamilyName);
+			if (!options.loadedFonts || options.loadedFonts.find(font => normalizeFontFamily(font.family) == fontFamilyName && font.style == fontStyle)) {
+				const fontWeight = getFontWeight(computedStyle.getPropertyValue("font-weight"));
+				const fontVariant = computedStyle.getPropertyValue("font-variant") || "normal";
+				const value = [fontFamilyName, fontWeight, fontStyle, fontVariant];
+				usedFonts.set(JSON.stringify(value), [fontFamilyName, fontWeight, fontStyle, fontVariant]);
 			}
-		} else {
+		});
+	}
+}
+
+function getShadowRoot(element) {
+	const chrome = globalThis.chrome;
+	if (element.openOrClosedShadowRoot) {
+		return element.openOrClosedShadowRoot;
+	} else if (chrome && chrome.dom && chrome.dom.openOrClosedShadowRoot) {
+		try {
+			return chrome.dom.openOrClosedShadowRoot(element);
+		} catch (error) {
 			return element.shadowRoot;
 		}
+	} else {
+		return element.shadowRoot;
 	}
+}
 
-	function normalizeFontFamily(fontFamilyName = "") {
-		return removeQuotes(singlefile.lib.vendor.cssUnescape.process(fontFamilyName.trim())).toLowerCase();
-	}
+function normalizeFontFamily(fontFamilyName = "") {
+	return removeQuotes(cssUnescape.process(fontFamilyName.trim())).toLowerCase();
+}
 
-	function testHiddenElement(element, computedStyle) {
-		let hidden = false;
-		if (computedStyle) {
-			const display = computedStyle.getPropertyValue("display");
-			const opacity = computedStyle.getPropertyValue("opacity");
-			const visibility = computedStyle.getPropertyValue("visibility");
-			hidden = display == "none";
-			if (!hidden && (opacity == "0" || visibility == "hidden") && element.getBoundingClientRect) {
-				const boundingRect = element.getBoundingClientRect();
-				hidden = !boundingRect.width && !boundingRect.height;
-			}
+function testHiddenElement(element, computedStyle) {
+	let hidden = false;
+	if (computedStyle) {
+		const display = computedStyle.getPropertyValue("display");
+		const opacity = computedStyle.getPropertyValue("opacity");
+		const visibility = computedStyle.getPropertyValue("visibility");
+		hidden = display == "none";
+		if (!hidden && (opacity == "0" || visibility == "hidden") && element.getBoundingClientRect) {
+			const boundingRect = element.getBoundingClientRect();
+			hidden = !boundingRect.width && !boundingRect.height;
 		}
-		return Boolean(hidden);
 	}
+	return Boolean(hidden);
+}
 
-	function postProcessDoc(doc, markedElements) {
-		doc.querySelectorAll("[" + DISABLED_NOSCRIPT_ATTRIBUTE_NAME + "]").forEach(element => {
-			element.textContent = element.getAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME);
-			element.removeAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME);
-		});
-		doc.querySelectorAll("meta[disabled-http-equiv]").forEach(element => {
-			element.setAttribute("http-equiv", element.getAttribute("disabled-http-equiv"));
-			element.removeAttribute("disabled-http-equiv");
-		});
-		if (doc.head) {
-			doc.head.querySelectorAll("*:not(base):not(link):not(meta):not(noscript):not(script):not(style):not(template):not(title)").forEach(element => element.removeAttribute("hidden"));
-		}
-		if (!markedElements) {
-			const singleFileAttributes = [REMOVED_CONTENT_ATTRIBUTE_NAME, HIDDEN_FRAME_ATTRIBUTE_NAME, HIDDEN_CONTENT_ATTRIBUTE_NAME, PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME, IMAGE_ATTRIBUTE_NAME, POSTER_ATTRIBUTE_NAME, CANVAS_ATTRIBUTE_NAME, INPUT_VALUE_ATTRIBUTE_NAME, SHADOW_ROOT_ATTRIBUTE_NAME, HTML_IMPORT_ATTRIBUTE_NAME, STYLESHEET_ATTRIBUTE_NAME, ASYNC_SCRIPT_ATTRIBUTE_NAME];
-			markedElements = doc.querySelectorAll(singleFileAttributes.map(name => "[" + name + "]").join(","));
-		}
-		markedElements.forEach(element => {
-			element.removeAttribute(REMOVED_CONTENT_ATTRIBUTE_NAME);
-			element.removeAttribute(HIDDEN_CONTENT_ATTRIBUTE_NAME);
-			element.removeAttribute(KEPT_CONTENT_ATTRIBUTE_NAME);
-			element.removeAttribute(HIDDEN_FRAME_ATTRIBUTE_NAME);
-			element.removeAttribute(PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME);
-			element.removeAttribute(IMAGE_ATTRIBUTE_NAME);
-			element.removeAttribute(POSTER_ATTRIBUTE_NAME);
-			element.removeAttribute(CANVAS_ATTRIBUTE_NAME);
-			element.removeAttribute(INPUT_VALUE_ATTRIBUTE_NAME);
-			element.removeAttribute(SHADOW_ROOT_ATTRIBUTE_NAME);
-			element.removeAttribute(HTML_IMPORT_ATTRIBUTE_NAME);
-			element.removeAttribute(STYLESHEET_ATTRIBUTE_NAME);
-			element.removeAttribute(ASYNC_SCRIPT_ATTRIBUTE_NAME);
-		});
+function postProcessDoc(doc, markedElements) {
+	doc.querySelectorAll("[" + DISABLED_NOSCRIPT_ATTRIBUTE_NAME + "]").forEach(element => {
+		element.textContent = element.getAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME);
+		element.removeAttribute(DISABLED_NOSCRIPT_ATTRIBUTE_NAME);
+	});
+	doc.querySelectorAll("meta[disabled-http-equiv]").forEach(element => {
+		element.setAttribute("http-equiv", element.getAttribute("disabled-http-equiv"));
+		element.removeAttribute("disabled-http-equiv");
+	});
+	if (doc.head) {
+		doc.head.querySelectorAll("*:not(base):not(link):not(meta):not(noscript):not(script):not(style):not(template):not(title)").forEach(element => element.removeAttribute("hidden"));
 	}
-
-	function getStylesheetsData(doc) {
-		if (doc) {
-			const contents = [];
-			doc.querySelectorAll("style").forEach((styleElement, styleIndex) => {
-				try {
-					const tempStyleElement = doc.createElement("style");
-					tempStyleElement.textContent = styleElement.textContent;
-					doc.body.appendChild(tempStyleElement);
-					const stylesheet = tempStyleElement.sheet;
-					tempStyleElement.remove();
-					if (!stylesheet || stylesheet.cssRules.length != styleElement.sheet.cssRules.length) {
-						styleElement.setAttribute(STYLESHEET_ATTRIBUTE_NAME, styleIndex);
-						contents[styleIndex] = Array.from(styleElement.sheet.cssRules).map(cssRule => cssRule.cssText).join("\n");
-					}
-				} catch (error) {
-					// ignored
-				}
-			});
-			return contents;
-		}
+	if (!markedElements) {
+		const singleFileAttributes = [REMOVED_CONTENT_ATTRIBUTE_NAME, HIDDEN_FRAME_ATTRIBUTE_NAME, HIDDEN_CONTENT_ATTRIBUTE_NAME, PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME, IMAGE_ATTRIBUTE_NAME, POSTER_ATTRIBUTE_NAME, CANVAS_ATTRIBUTE_NAME, INPUT_VALUE_ATTRIBUTE_NAME, SHADOW_ROOT_ATTRIBUTE_NAME, HTML_IMPORT_ATTRIBUTE_NAME, STYLESHEET_ATTRIBUTE_NAME, ASYNC_SCRIPT_ATTRIBUTE_NAME];
+		markedElements = doc.querySelectorAll(singleFileAttributes.map(name => "[" + name + "]").join(","));
 	}
+	markedElements.forEach(element => {
+		element.removeAttribute(REMOVED_CONTENT_ATTRIBUTE_NAME);
+		element.removeAttribute(HIDDEN_CONTENT_ATTRIBUTE_NAME);
+		element.removeAttribute(KEPT_CONTENT_ATTRIBUTE_NAME);
+		element.removeAttribute(HIDDEN_FRAME_ATTRIBUTE_NAME);
+		element.removeAttribute(PRESERVED_SPACE_ELEMENT_ATTRIBUTE_NAME);
+		element.removeAttribute(IMAGE_ATTRIBUTE_NAME);
+		element.removeAttribute(POSTER_ATTRIBUTE_NAME);
+		element.removeAttribute(CANVAS_ATTRIBUTE_NAME);
+		element.removeAttribute(INPUT_VALUE_ATTRIBUTE_NAME);
+		element.removeAttribute(SHADOW_ROOT_ATTRIBUTE_NAME);
+		element.removeAttribute(HTML_IMPORT_ATTRIBUTE_NAME);
+		element.removeAttribute(STYLESHEET_ATTRIBUTE_NAME);
+		element.removeAttribute(ASYNC_SCRIPT_ATTRIBUTE_NAME);
+	});
+}
 
-	function getSize(win, imageElement, computedStyle) {
-		let pxWidth = imageElement.naturalWidth;
-		let pxHeight = imageElement.naturalHeight;
-		if (!pxWidth && !pxHeight) {
-			computedStyle = computedStyle || win.getComputedStyle(imageElement);
-			let removeBorderWidth = false;
-			if (computedStyle.getPropertyValue("box-sizing") == "content-box") {
-				const boxSizingValue = imageElement.style.getPropertyValue("box-sizing");
-				const boxSizingPriority = imageElement.style.getPropertyPriority("box-sizing");
-				const clientWidth = imageElement.clientWidth;
-				imageElement.style.setProperty("box-sizing", "border-box", "important");
-				removeBorderWidth = imageElement.clientWidth != clientWidth;
-				if (boxSizingValue) {
-					imageElement.style.setProperty("box-sizing", boxSizingValue, boxSizingPriority);
-				} else {
-					imageElement.style.removeProperty("box-sizing");
+function getStylesheetsData(doc) {
+	if (doc) {
+		const contents = [];
+		doc.querySelectorAll("style").forEach((styleElement, styleIndex) => {
+			try {
+				const tempStyleElement = doc.createElement("style");
+				tempStyleElement.textContent = styleElement.textContent;
+				doc.body.appendChild(tempStyleElement);
+				const stylesheet = tempStyleElement.sheet;
+				tempStyleElement.remove();
+				if (!stylesheet || stylesheet.cssRules.length != styleElement.sheet.cssRules.length) {
+					styleElement.setAttribute(STYLESHEET_ATTRIBUTE_NAME, styleIndex);
+					contents[styleIndex] = Array.from(styleElement.sheet.cssRules).map(cssRule => cssRule.cssText).join("\n");
 				}
+			} catch (error) {
+				// ignored
 			}
-			let paddingLeft, paddingRight, paddingTop, paddingBottom, borderLeft, borderRight, borderTop, borderBottom;
-			paddingLeft = getWidth("padding-left", computedStyle);
-			paddingRight = getWidth("padding-right", computedStyle);
-			paddingTop = getWidth("padding-top", computedStyle);
-			paddingBottom = getWidth("padding-bottom", computedStyle);
-			if (removeBorderWidth) {
-				borderLeft = getWidth("border-left-width", computedStyle);
-				borderRight = getWidth("border-right-width", computedStyle);
-				borderTop = getWidth("border-top-width", computedStyle);
-				borderBottom = getWidth("border-bottom-width", computedStyle);
+		});
+		return contents;
+	}
+}
+
+function getSize(win, imageElement, computedStyle) {
+	let pxWidth = imageElement.naturalWidth;
+	let pxHeight = imageElement.naturalHeight;
+	if (!pxWidth && !pxHeight) {
+		computedStyle = computedStyle || win.getComputedStyle(imageElement);
+		let removeBorderWidth = false;
+		if (computedStyle.getPropertyValue("box-sizing") == "content-box") {
+			const boxSizingValue = imageElement.style.getPropertyValue("box-sizing");
+			const boxSizingPriority = imageElement.style.getPropertyPriority("box-sizing");
+			const clientWidth = imageElement.clientWidth;
+			imageElement.style.setProperty("box-sizing", "border-box", "important");
+			removeBorderWidth = imageElement.clientWidth != clientWidth;
+			if (boxSizingValue) {
+				imageElement.style.setProperty("box-sizing", boxSizingValue, boxSizingPriority);
 			} else {
-				borderLeft = borderRight = borderTop = borderBottom = 0;
+				imageElement.style.removeProperty("box-sizing");
 			}
-			pxWidth = Math.max(0, imageElement.clientWidth - paddingLeft - paddingRight - borderLeft - borderRight);
-			pxHeight = Math.max(0, imageElement.clientHeight - paddingTop - paddingBottom - borderTop - borderBottom);
 		}
-		return { pxWidth, pxHeight };
-	}
-
-	function getWidth(styleName, computedStyle) {
-		if (computedStyle.getPropertyValue(styleName).endsWith("px")) {
-			return parseFloat(computedStyle.getPropertyValue(styleName));
+		let paddingLeft, paddingRight, paddingTop, paddingBottom, borderLeft, borderRight, borderTop, borderBottom;
+		paddingLeft = getWidth("padding-left", computedStyle);
+		paddingRight = getWidth("padding-right", computedStyle);
+		paddingTop = getWidth("padding-top", computedStyle);
+		paddingBottom = getWidth("padding-bottom", computedStyle);
+		if (removeBorderWidth) {
+			borderLeft = getWidth("border-left-width", computedStyle);
+			borderRight = getWidth("border-right-width", computedStyle);
+			borderTop = getWidth("border-top-width", computedStyle);
+			borderBottom = getWidth("border-bottom-width", computedStyle);
+		} else {
+			borderLeft = borderRight = borderTop = borderBottom = 0;
 		}
+		pxWidth = Math.max(0, imageElement.clientWidth - paddingLeft - paddingRight - borderLeft - borderRight);
+		pxHeight = Math.max(0, imageElement.clientHeight - paddingTop - paddingBottom - borderTop - borderBottom);
 	}
+	return { pxWidth, pxHeight };
+}
 
-	function getFontsData() {
-		if (singlefile.lib.processors.hooks.content.frames) {
-			return singlefile.lib.processors.hooks.content.frames.getFontsData();
-		}
+function getWidth(styleName, computedStyle) {
+	if (computedStyle.getPropertyValue(styleName).endsWith("px")) {
+		return parseFloat(computedStyle.getPropertyValue(styleName));
 	}
+}
 
-	function serialize(doc) {
-		const docType = doc.doctype;
-		let docTypeString = "";
-		if (docType) {
-			docTypeString = "<!DOCTYPE " + docType.nodeName;
-			if (docType.publicId) {
-				docTypeString += " PUBLIC \"" + docType.publicId + "\"";
-				if (docType.systemId) {
-					docTypeString += " \"" + docType.systemId + "\"";
-				}
-			} else if (docType.systemId) {
-				docTypeString += " SYSTEM \"" + docType.systemId + "\"";
-			} if (docType.internalSubset) {
-				docTypeString += " [" + docType.internalSubset + "]";
-			}
-			docTypeString += "> ";
-		}
-		return docTypeString + doc.documentElement.outerHTML;
-	}
+function getFontsData() {
+	return hooksFrames.getFontsData();
+}
 
-	function removeQuotes(string) {
-		if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
-			string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
-		} else {
-			string = string.replace(REGEXP_DOUBLE_QUOTES_STRING, "$1");
+function serialize(doc) {
+	const docType = doc.doctype;
+	let docTypeString = "";
+	if (docType) {
+		docTypeString = "<!DOCTYPE " + docType.nodeName;
+		if (docType.publicId) {
+			docTypeString += " PUBLIC \"" + docType.publicId + "\"";
+			if (docType.systemId) {
+				docTypeString += " \"" + docType.systemId + "\"";
+			}
+		} else if (docType.systemId) {
+			docTypeString += " SYSTEM \"" + docType.systemId + "\"";
+		} if (docType.internalSubset) {
+			docTypeString += " [" + docType.internalSubset + "]";
 		}
-		return string.trim();
+		docTypeString += "> ";
 	}
+	return docTypeString + doc.documentElement.outerHTML;
+}
 
-	function getFontWeight(weight) {
-		return FONT_WEIGHTS[weight.toLowerCase().trim()] || weight;
+function removeQuotes(string) {
+	if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
+		string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
+	} else {
+		string = string.replace(REGEXP_DOUBLE_QUOTES_STRING, "$1");
 	}
+	return string.trim();
+}
 
-	function flatten(array) {
-		return array.flat ? array.flat() : array.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
-	}
+function getFontWeight(weight) {
+	return FONT_WEIGHTS[weight.toLowerCase().trim()] || weight;
+}
 
-})(typeof globalThis == "object" ? globalThis : window);
+function flatten(array) {
+	return array.flat ? array.flat() : array.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
+}

+ 287 - 318
lib/single-file/single-file-util.js

@@ -21,362 +21,331 @@
  *   Source.
  */
 
-/* global window, globalThis */
+/* global globalThis */
 
-this.singlefile.lib.util = this.singlefile.lib.util || (globalThis => {
+import * as vendor from "./vendor/index.js";
+import * as modules from "./modules/index.js";
+import * as helper from "./single-file-helper.js";
 
-	const DEBUG = false;
-	const ONE_MB = 1024 * 1024;
-	const PREFIX_CONTENT_TYPE_TEXT = "text/";
-	const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "\\\\", "?", "%", "*", ":", "|", "\"", "<", ">", "\x00-\x1f", "\x7F"];
-	const DEFAULT_REPLACEMENT_CHARACTER = "_";
+const DEBUG = false;
+const ONE_MB = 1024 * 1024;
+const PREFIX_CONTENT_TYPE_TEXT = "text/";
+const DEFAULT_REPLACED_CHARACTERS = ["~", "+", "\\\\", "?", "%", "*", ":", "|", "\"", "<", ">", "\x00-\x1f", "\x7F"];
+const DEFAULT_REPLACEMENT_CHARACTER = "_";
 
-	const URL = globalThis.URL;
-	const DOMParser = globalThis.DOMParser;
-	const Blob = globalThis.Blob;
-	const FileReader = globalThis.FileReader;
-	const fetch = url => globalThis.fetch(url);
-	const crypto = globalThis.crypto;
-	const TextDecoder = globalThis.TextDecoder;
-	const TextEncoder = globalThis.TextEncoder;
-	const singlefile = this.singlefile;
+const URL = globalThis.URL;
+const DOMParser = globalThis.DOMParser;
+const Blob = globalThis.Blob;
+const FileReader = globalThis.FileReader;
+const fetch = url => globalThis.fetch(url);
+const crypto = globalThis.crypto;
+const TextDecoder = globalThis.TextDecoder;
+const TextEncoder = globalThis.TextEncoder;
 
+export {
+	getInstance
+};
+
+function getInstance(utilOptions) {
+	utilOptions = utilOptions || {};
+	utilOptions.fetch = utilOptions.fetch || fetch;
+	utilOptions.frameFetch = utilOptions.frameFetch || utilOptions.fetch || fetch;
 	return {
-		getInstance
+		getContent,
+		parseURL(resourceURL, baseURI) {
+			if (baseURI === undefined) {
+				return new URL(resourceURL);
+			} else {
+				return new URL(resourceURL, baseURI);
+			}
+		},
+		resolveURL(resourceURL, baseURI) {
+			return this.parseURL(resourceURL, baseURI).href;
+		},
+		getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARACTERS, replacementCharacter = DEFAULT_REPLACEMENT_CHARACTER) {
+			replacedCharacters.forEach(replacedCharacter => filename = filename.replace(new RegExp("[" + replacedCharacter + "]+", "g"), replacementCharacter));
+			filename = filename
+				.replace(/\.\.\//g, "")
+				.replace(/^\/+/, "")
+				.replace(/\/+/g, "/")
+				.replace(/\/$/, "")
+				.replace(/\.$/, "")
+				.replace(/\.\//g, "." + replacementCharacter)
+				.replace(/\/\./g, "/" + replacementCharacter);
+			return filename;
+		},
+		parseDocContent(content, baseURI) {
+			const doc = (new DOMParser()).parseFromString(content, "text/html");
+			if (!doc.head) {
+				doc.documentElement.insertBefore(doc.createElement("HEAD"), doc.body);
+			}
+			let baseElement = doc.querySelector("base");
+			if (!baseElement || !baseElement.getAttribute("href")) {
+				if (baseElement) {
+					baseElement.remove();
+				}
+				baseElement = doc.createElement("base");
+				baseElement.setAttribute("href", baseURI);
+				doc.head.insertBefore(baseElement, doc.head.firstChild);
+			}
+			return doc;
+		},
+		parseXMLContent(content) {
+			return (new DOMParser()).parseFromString(content, "text/xml");
+		},
+		parseSVGContent(content) {
+			return (new DOMParser()).parseFromString(content, "image/svg+xml");
+		},
+		async digest(algo, text) {
+			try {
+				const hash = await crypto.subtle.digest(algo, new TextEncoder("utf-8").encode(text));
+				return hex(hash);
+			} catch (error) {
+				return "";
+			}
+		},
+		getContentSize(content) {
+			return new Blob([content]).size;
+		},
+		truncateText(content, maxSize) {
+			const blob = new Blob([content]);
+			const reader = new FileReader();
+			reader.readAsText(blob.slice(0, maxSize));
+			return new Promise((resolve, reject) => {
+				reader.addEventListener("load", () => {
+					if (content.startsWith(reader.result)) {
+						resolve(reader.result);
+					} else {
+						this.truncateText(content, maxSize - 1).then(resolve).catch(reject);
+					}
+				}, false);
+				reader.addEventListener("error", reject, false);
+			});
+		},
+		minifyHTML(doc, options) {
+			return modules.htmlMinifier.process(doc, options);
+		},
+		minifyCSSRules(stylesheets, styles, mediaAllInfo) {
+			return modules.cssRulesMinifier.process(stylesheets, styles, mediaAllInfo);
+		},
+		removeUnusedFonts(doc, stylesheets, styles, options) {
+			return modules.fontsMinifier.process(doc, stylesheets, styles, options);
+		},
+		removeAlternativeFonts(doc, stylesheets, fontURLs, fontTests) {
+			return modules.fontsAltMinifier.process(doc, stylesheets, fontURLs, fontTests);
+		},
+		getMediaAllInfo(doc, stylesheets, styles) {
+			return modules.matchedRules.getMediaAllInfo(doc, stylesheets, styles);
+		},
+		compressCSS(content, options) {
+			return vendor.cssMinifier.processString(content, options);
+		},
+		minifyMedias(stylesheets) {
+			return modules.mediasAltMinifier.process(stylesheets);
+		},
+		removeAlternativeImages(doc) {
+			return modules.imagesAltMinifier.process(doc);
+		},
+		parseSrcset(srcset) {
+			return vendor.srcsetParser.process(srcset);
+		},
+		preProcessDoc(doc, win, options) {
+			return helper.preProcessDoc(doc, win, options);
+		},
+		postProcessDoc(doc, markedElements) {
+			helper.postProcessDoc(doc, markedElements);
+		},
+		serialize(doc, compressHTML) {
+			return modules.serializer.process(doc, compressHTML);
+		},
+		removeQuotes(string) {
+			return helper.removeQuotes(string);
+		},
+		waitForUserScript: helper.waitForUserScript,
+		ON_BEFORE_CAPTURE_EVENT_NAME: helper.ON_BEFORE_CAPTURE_EVENT_NAME,
+		ON_AFTER_CAPTURE_EVENT_NAME: helper.ON_AFTER_CAPTURE_EVENT_NAME,
+		WIN_ID_ATTRIBUTE_NAME: helper.WIN_ID_ATTRIBUTE_NAME,
+		REMOVED_CONTENT_ATTRIBUTE_NAME: helper.REMOVED_CONTENT_ATTRIBUTE_NAME,
+		HIDDEN_CONTENT_ATTRIBUTE_NAME: helper.HIDDEN_CONTENT_ATTRIBUTE_NAME,
+		HIDDEN_FRAME_ATTRIBUTE_NAME: helper.HIDDEN_FRAME_ATTRIBUTE_NAME,
+		IMAGE_ATTRIBUTE_NAME: helper.IMAGE_ATTRIBUTE_NAME,
+		POSTER_ATTRIBUTE_NAME: helper.POSTER_ATTRIBUTE_NAME,
+		CANVAS_ATTRIBUTE_NAME: helper.CANVAS_ATTRIBUTE_NAME,
+		HTML_IMPORT_ATTRIBUTE_NAME: helper.HTML_IMPORT_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,
+		STYLESHEET_ATTRIBUTE_NAME: helper.STYLESHEET_ATTRIBUTE_NAME,
+		SELECTED_CONTENT_ATTRIBUTE_NAME: helper.SELECTED_CONTENT_ATTRIBUTE_NAME,
+		COMMENT_HEADER: helper.COMMENT_HEADER,
+		COMMENT_HEADER_LEGACY: helper.COMMENT_HEADER_LEGACY,
+		SINGLE_FILE_UI_ELEMENT_CLASS: helper.SINGLE_FILE_UI_ELEMENT_CLASS
 	};
 
-	function getInstance(utilOptions) {
-		const modules = singlefile.lib.modules;
-		const vendor = singlefile.lib.vendor;
-		const helper = singlefile.lib.helper;
-
-		if (modules.serializer === undefined) {
-			modules.serializer = {
-				process(doc) {
-					const docType = doc.doctype;
-					let docTypeString = "";
-					if (docType) {
-						docTypeString = "<!DOCTYPE " + docType.nodeName;
-						if (docType.publicId) {
-							docTypeString += " PUBLIC \"" + docType.publicId + "\"";
-							if (docType.systemId)
-								docTypeString += " \"" + docType.systemId + "\"";
-						} else if (docType.systemId)
-							docTypeString += " SYSTEM \"" + docType.systemId + "\"";
-						if (docType.internalSubset)
-							docTypeString += " [" + docType.internalSubset + "]";
-						docTypeString += "> ";
-					}
-					return docTypeString + doc.documentElement.outerHTML;
-				}
-			};
+	async function getContent(resourceURL, options) {
+		let response, startTime;
+		const fetchResource = utilOptions.fetch;
+		const fetchFrameResource = utilOptions.frameFetch;
+		if (DEBUG) {
+			startTime = Date.now();
+			log("  // STARTED download url =", resourceURL, "asBinary =", options.asBinary);
 		}
-
-		utilOptions = utilOptions || {};
-		utilOptions.fetch = utilOptions.fetch || fetch;
-		utilOptions.frameFetch = utilOptions.frameFetch || utilOptions.fetch || fetch;
-		return {
-			getContent,
-			parseURL(resourceURL, baseURI) {
-				if (baseURI === undefined) {
-					return new URL(resourceURL);
-				} else {
-					return new URL(resourceURL, baseURI);
-				}
-			},
-			resolveURL(resourceURL, baseURI) {
-				return this.parseURL(resourceURL, baseURI).href;
-			},
-			getValidFilename(filename, replacedCharacters = DEFAULT_REPLACED_CHARACTERS, replacementCharacter = DEFAULT_REPLACEMENT_CHARACTER) {
-				replacedCharacters.forEach(replacedCharacter => filename = filename.replace(new RegExp("[" + replacedCharacter + "]+", "g"), replacementCharacter));
-				filename = filename
-					.replace(/\.\.\//g, "")
-					.replace(/^\/+/, "")
-					.replace(/\/+/g, "/")
-					.replace(/\/$/, "")
-					.replace(/\.$/, "")
-					.replace(/\.\//g, "." + replacementCharacter)
-					.replace(/\/\./g, "/" + replacementCharacter);
-				return filename;
-			},
-			parseDocContent(content, baseURI) {
-				const doc = (new DOMParser()).parseFromString(content, "text/html");
-				if (!doc.head) {
-					doc.documentElement.insertBefore(doc.createElement("HEAD"), doc.body);
-				}
-				let baseElement = doc.querySelector("base");
-				if (!baseElement || !baseElement.getAttribute("href")) {
-					if (baseElement) {
-						baseElement.remove();
-					}
-					baseElement = doc.createElement("base");
-					baseElement.setAttribute("href", baseURI);
-					doc.head.insertBefore(baseElement, doc.head.firstChild);
-				}
-				return doc;
-			},
-			parseXMLContent(content) {
-				return (new DOMParser()).parseFromString(content, "text/xml");
-			},
-			parseSVGContent(content) {
-				return (new DOMParser()).parseFromString(content, "image/svg+xml");
-			},
-			async digest(algo, text) {
+		try {
+			if (options.frameId) {
 				try {
-					const hash = await crypto.subtle.digest(algo, new TextEncoder("utf-8").encode(text));
-					return hex(hash);
+					response = await fetchFrameResource(resourceURL, { frameId: options.frameId, referrer: options.resourceReferrer });
 				} catch (error) {
-					return "";
+					response = await fetchResource(resourceURL);
 				}
-			},
-			getContentSize(content) {
-				return new Blob([content]).size;
-			},
-			truncateText(content, maxSize) {
-				const blob = new Blob([content]);
-				const reader = new FileReader();
-				reader.readAsText(blob.slice(0, maxSize));
-				return new Promise((resolve, reject) => {
-					reader.addEventListener("load", () => {
-						if (content.startsWith(reader.result)) {
-							resolve(reader.result);
-						} else {
-							this.truncateText(content, maxSize - 1).then(resolve).catch(reject);
-						}
-					}, false);
-					reader.addEventListener("error", reject, false);
-				});
-			},
-			minifyHTML(doc, options) {
-				return modules.htmlMinifier.process(doc, options);
-			},
-			minifyCSSRules(stylesheets, styles, mediaAllInfo) {
-				return modules.cssRulesMinifier.process(stylesheets, styles, mediaAllInfo);
-			},
-			removeUnusedFonts(doc, stylesheets, styles, options) {
-				return modules.fontsMinifier.process(doc, stylesheets, styles, options);
-			},
-			removeAlternativeFonts(doc, stylesheets, fontURLs, fontTests) {
-				return modules.fontsAltMinifier.process(doc, stylesheets, fontURLs, fontTests);
-			},
-			getMediaAllInfo(doc, stylesheets, styles) {
-				return modules.matchedRules.getMediaAllInfo(doc, stylesheets, styles);
-			},
-			compressCSS(content, options) {
-				return vendor.cssMinifier.processString(content, options);
-			},
-			minifyMedias(stylesheets) {
-				return modules.mediasAltMinifier.process(stylesheets);
-			},
-			removeAlternativeImages(doc) {
-				return modules.imagesAltMinifier.process(doc);
-			},
-			parseSrcset(srcset) {
-				return vendor.srcsetParser.process(srcset);
-			},
-			preProcessDoc(doc, win, options) {
-				return helper.preProcessDoc(doc, win, options);
-			},
-			postProcessDoc(doc, markedElements) {
-				helper.postProcessDoc(doc, markedElements);
-			},
-			serialize(doc, compressHTML) {
-				return modules.serializer.process(doc, compressHTML);
-			},
-			removeQuotes(string) {
-				return helper.removeQuotes(string);
-			},
-			waitForUserScript(eventPrefixName) {
-				if (helper.waitForUserScript) {
-					return helper.waitForUserScript(eventPrefixName);
-				}
-			},
-			ON_BEFORE_CAPTURE_EVENT_NAME: helper.ON_BEFORE_CAPTURE_EVENT_NAME,
-			ON_AFTER_CAPTURE_EVENT_NAME: helper.ON_AFTER_CAPTURE_EVENT_NAME,
-			WIN_ID_ATTRIBUTE_NAME: helper.WIN_ID_ATTRIBUTE_NAME,
-			REMOVED_CONTENT_ATTRIBUTE_NAME: helper.REMOVED_CONTENT_ATTRIBUTE_NAME,
-			HIDDEN_CONTENT_ATTRIBUTE_NAME: helper.HIDDEN_CONTENT_ATTRIBUTE_NAME,
-			HIDDEN_FRAME_ATTRIBUTE_NAME: helper.HIDDEN_FRAME_ATTRIBUTE_NAME,
-			IMAGE_ATTRIBUTE_NAME: helper.IMAGE_ATTRIBUTE_NAME,
-			POSTER_ATTRIBUTE_NAME: helper.POSTER_ATTRIBUTE_NAME,
-			CANVAS_ATTRIBUTE_NAME: helper.CANVAS_ATTRIBUTE_NAME,
-			HTML_IMPORT_ATTRIBUTE_NAME: helper.HTML_IMPORT_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,
-			STYLESHEET_ATTRIBUTE_NAME: helper.STYLESHEET_ATTRIBUTE_NAME,
-			SELECTED_CONTENT_ATTRIBUTE_NAME: helper.SELECTED_CONTENT_ATTRIBUTE_NAME,
-			COMMENT_HEADER: helper.COMMENT_HEADER,
-			COMMENT_HEADER_LEGACY: helper.COMMENT_HEADER_LEGACY,
-			SINGLE_FILE_UI_ELEMENT_CLASS: helper.SINGLE_FILE_UI_ELEMENT_CLASS
-		};
-
-		async function getContent(resourceURL, options) {
-			let response, startTime;
-			const fetchResource = utilOptions.fetch;
-			const fetchFrameResource = utilOptions.frameFetch;
-			if (DEBUG) {
-				startTime = Date.now();
-				log("  // STARTED download url =", resourceURL, "asBinary =", options.asBinary);
+			} else {
+				response = await fetchResource(resourceURL, { referrer: options.resourceReferrer });
 			}
+		} catch (error) {
+			return { data: options.asBinary ? "data:null;base64," : "", resourceURL };
+		}
+		let buffer;
+		try {
+			buffer = await response.arrayBuffer();
+		} catch (error) {
+			return { data: options.asBinary ? "data:null;base64," : "", resourceURL };
+		}
+		resourceURL = response.url || resourceURL;
+		let contentType = "", charset;
+		try {
+			const mimeType = new vendor.MIMEType(response.headers.get("content-type"));
+			contentType = mimeType.type + "/" + mimeType.subtype;
+			charset = mimeType.parameters.get("charset");
+		} catch (error) {
+			// ignored
+		}
+		if (!contentType) {
+			contentType = guessMIMEType(options.expectedType, buffer);
+		}
+		if (!charset && options.charset) {
+			charset = options.charset;
+		}
+		if (options.asBinary) {
 			try {
-				if (options.frameId) {
-					try {
-						response = await fetchFrameResource(resourceURL, { frameId: options.frameId, referrer: options.resourceReferrer });
-					} catch (error) {
-						response = await fetchResource(resourceURL);
-					}
+				if (DEBUG) {
+					log("  // ENDED   download url =", resourceURL, "delay =", Date.now() - startTime);
+				}
+				if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
+					return { data: "data:null;base64,", resourceURL };
 				} else {
-					response = await fetchResource(resourceURL, { referrer: options.resourceReferrer });
+					const reader = new FileReader();
+					reader.readAsDataURL(new Blob([buffer], { type: contentType + (options.charset ? ";charset=" + options.charset : "") }));
+					const dataUri = await new Promise((resolve, reject) => {
+						reader.addEventListener("load", () => resolve(reader.result), false);
+						reader.addEventListener("error", reject, false);
+					});
+					return { data: dataUri, resourceURL };
 				}
 			} catch (error) {
-				return { data: options.asBinary ? "data:null;base64," : "", resourceURL };
+				return { data: "data:null;base64,", resourceURL };
 			}
-			let buffer;
-			try {
-				buffer = await response.arrayBuffer();
-			} catch (error) {
-				return { data: options.asBinary ? "data:null;base64," : "", resourceURL };
+		} else {
+			if (response.status >= 400 || (options.validateTextContentType && contentType && !contentType.startsWith(PREFIX_CONTENT_TYPE_TEXT))) {
+				return { data: "", resourceURL };
 			}
-			resourceURL = response.url || resourceURL;
-			let contentType = "", charset;
-			try {
-				const mimeType = new vendor.MIMEType(response.headers.get("content-type"));
-				contentType = mimeType.type + "/" + mimeType.subtype;
-				charset = mimeType.parameters.get("charset");
-			} catch (error) {
-				// ignored
-			}
-			if (!contentType) {
-				contentType = guessMIMEType(options.expectedType, buffer);
+			if (!charset) {
+				charset = "utf-8";
 			}
-			if (!charset && options.charset) {
-				charset = options.charset;
+			if (DEBUG) {
+				log("  // ENDED   download url =", resourceURL, "delay =", Date.now() - startTime);
 			}
-			if (options.asBinary) {
+			if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
+				return { data: "", resourceURL, charset };
+			} else {
 				try {
-					if (DEBUG) {
-						log("  // ENDED   download url =", resourceURL, "delay =", Date.now() - startTime);
-					}
-					if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
-						return { data: "data:null;base64,", resourceURL };
-					} else {
-						const reader = new FileReader();
-						reader.readAsDataURL(new Blob([buffer], { type: contentType + (options.charset ? ";charset=" + options.charset : "") }));
-						const dataUri = await new Promise((resolve, reject) => {
-							reader.addEventListener("load", () => resolve(reader.result), false);
-							reader.addEventListener("error", reject, false);
-						});
-						return { data: dataUri, resourceURL };
-					}
+					return { data: new TextDecoder(charset).decode(buffer), resourceURL, charset };
 				} catch (error) {
-					return { data: "data:null;base64,", resourceURL };
-				}
-			} else {
-				if (response.status >= 400 || (options.validateTextContentType && contentType && !contentType.startsWith(PREFIX_CONTENT_TYPE_TEXT))) {
-					return { data: "", resourceURL };
-				}
-				if (!charset) {
-					charset = "utf-8";
-				}
-				if (DEBUG) {
-					log("  // ENDED   download url =", resourceURL, "delay =", Date.now() - startTime);
-				}
-				if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
-					return { data: "", resourceURL, charset };
-				} else {
 					try {
+						charset = "utf-8";
 						return { data: new TextDecoder(charset).decode(buffer), resourceURL, charset };
 					} catch (error) {
-						try {
-							charset = "utf-8";
-							return { data: new TextDecoder(charset).decode(buffer), resourceURL, charset };
-						} catch (error) {
-							return { data: "", resourceURL, charset };
-						}
+						return { data: "", resourceURL, charset };
 					}
 				}
 			}
 		}
 	}
+}
 
-	function guessMIMEType(expectedType, buffer) {
-		if (expectedType == "image") {
-			if (compareBytes([255, 255, 255, 255], [0, 0, 1, 0])) {
-				return "image/x-icon";
-			}
-			if (compareBytes([255, 255, 255, 255], [0, 0, 2, 0])) {
-				return "image/x-icon";
-			}
-			if (compareBytes([255, 255], [78, 77])) {
-				return "image/bmp";
-			}
-			if (compareBytes([255, 255, 255, 255, 255, 255], [71, 73, 70, 56, 57, 97])) {
-				return "image/gif";
-			}
-			if (compareBytes([255, 255, 255, 255, 255, 255], [71, 73, 70, 56, 59, 97])) {
-				return "image/gif";
-			}
-			if (compareBytes([255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255], [82, 73, 70, 70, 0, 0, 0, 0, 87, 69, 66, 80, 86, 80])) {
-				return "image/webp";
-			}
-			if (compareBytes([255, 255, 255, 255, 255, 255, 255, 255], [137, 80, 78, 71, 13, 10, 26, 10])) {
-				return "image/png";
-			}
-			if (compareBytes([255, 255, 255], [255, 216, 255])) {
-				return "image/jpeg";
-			}
+function guessMIMEType(expectedType, buffer) {
+	if (expectedType == "image") {
+		if (compareBytes([255, 255, 255, 255], [0, 0, 1, 0])) {
+			return "image/x-icon";
 		}
-		if (expectedType == "font") {
-			if (compareBytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255],
-				[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 76, 80])) {
-				return "application/vnd.ms-fontobject";
-			}
-			if (compareBytes([255, 255, 255, 255], [0, 1, 0, 0])) {
-				return "font/ttf";
-			}
-			if (compareBytes([255, 255, 255, 255], [79, 84, 84, 79])) {
-				return "font/otf";
-			}
-			if (compareBytes([255, 255, 255, 255], [116, 116, 99, 102])) {
-				return "font/collection";
-			}
-			if (compareBytes([255, 255, 255, 255], [119, 79, 70, 70])) {
-				return "font/woff";
-			}
-			if (compareBytes([255, 255, 255, 255], [119, 79, 70, 50])) {
-				return "font/woff2";
-			}
+		if (compareBytes([255, 255, 255, 255], [0, 0, 2, 0])) {
+			return "image/x-icon";
 		}
-
-		function compareBytes(mask, pattern) {
-			let patternMatch = true, index = 0;
-			if (buffer.byteLength >= pattern.length) {
-				const value = new Uint8Array(buffer, 0, mask.length);
-				for (index = 0; index < mask.length && patternMatch; index++) {
-					patternMatch = patternMatch && ((value[index] & mask[index]) == pattern[index]);
-				}
-				return patternMatch;
-			}
+		if (compareBytes([255, 255], [78, 77])) {
+			return "image/bmp";
+		}
+		if (compareBytes([255, 255, 255, 255, 255, 255], [71, 73, 70, 56, 57, 97])) {
+			return "image/gif";
+		}
+		if (compareBytes([255, 255, 255, 255, 255, 255], [71, 73, 70, 56, 59, 97])) {
+			return "image/gif";
+		}
+		if (compareBytes([255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255], [82, 73, 70, 70, 0, 0, 0, 0, 87, 69, 66, 80, 86, 80])) {
+			return "image/webp";
+		}
+		if (compareBytes([255, 255, 255, 255, 255, 255, 255, 255], [137, 80, 78, 71, 13, 10, 26, 10])) {
+			return "image/png";
+		}
+		if (compareBytes([255, 255, 255], [255, 216, 255])) {
+			return "image/jpeg";
+		}
+	}
+	if (expectedType == "font") {
+		if (compareBytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255],
+			[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 76, 80])) {
+			return "application/vnd.ms-fontobject";
+		}
+		if (compareBytes([255, 255, 255, 255], [0, 1, 0, 0])) {
+			return "font/ttf";
+		}
+		if (compareBytes([255, 255, 255, 255], [79, 84, 84, 79])) {
+			return "font/otf";
+		}
+		if (compareBytes([255, 255, 255, 255], [116, 116, 99, 102])) {
+			return "font/collection";
+		}
+		if (compareBytes([255, 255, 255, 255], [119, 79, 70, 70])) {
+			return "font/woff";
+		}
+		if (compareBytes([255, 255, 255, 255], [119, 79, 70, 50])) {
+			return "font/woff2";
 		}
 	}
 
-	// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
-	function hex(buffer) {
-		const hexCodes = [];
-		const view = new DataView(buffer);
-		for (let i = 0; i < view.byteLength; i += 4) {
-			const value = view.getUint32(i);
-			const stringValue = value.toString(16);
-			const padding = "00000000";
-			const paddedValue = (padding + stringValue).slice(-padding.length);
-			hexCodes.push(paddedValue);
+	function compareBytes(mask, pattern) {
+		let patternMatch = true, index = 0;
+		if (buffer.byteLength >= pattern.length) {
+			const value = new Uint8Array(buffer, 0, mask.length);
+			for (index = 0; index < mask.length && patternMatch; index++) {
+				patternMatch = patternMatch && ((value[index] & mask[index]) == pattern[index]);
+			}
+			return patternMatch;
 		}
-		return hexCodes.join("");
 	}
+}
 
-	function log(...args) {
-		console.log("S-File <browser>", ...args); // eslint-disable-line no-console
+// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
+function hex(buffer) {
+	const hexCodes = [];
+	const view = new DataView(buffer);
+	for (let i = 0; i < view.byteLength; i += 4) {
+		const value = view.getUint32(i);
+		const stringValue = value.toString(16);
+		const padding = "00000000";
+		const paddedValue = (padding + stringValue).slice(-padding.length);
+		hexCodes.push(paddedValue);
 	}
+	return hexCodes.join("");
+}
 
-})(typeof globalThis == "object" ? globalThis : window);
+function log(...args) {
+	console.log("S-File <browser>", ...args); // eslint-disable-line no-console
+}

+ 255 - 259
lib/single-file/vendor/css-font-property-parser.js

@@ -48,300 +48,296 @@
  * SOFTWARE.
  */
 
-this.singlefile.lib.vendor.fontPropertyParser = this.singlefile.lib.vendor.fontPropertyParser || (() => {
-
-	const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
-	const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
-
-	const globalKeywords = [
-		"inherit",
-		"initial",
-		"unset"
-	];
-
-	const systemFontKeywords = [
-		"caption",
-		"icon",
-		"menu",
-		"message-box",
-		"small-caption",
-		"status-bar"
-	];
-
-	const fontWeightKeywords = [
-		"normal",
-		"bold",
-		"bolder",
-		"lighter",
-		"100",
-		"200",
-		"300",
-		"400",
-		"500",
-		"600",
-		"700",
-		"800",
-		"900"
-	];
-
-	const fontStyleKeywords = [
-		"normal",
-		"italic",
-		"oblique"
-	];
-
-	const fontStretchKeywords = [
-		"normal",
-		"condensed",
-		"semi-condensed",
-		"extra-condensed",
-		"ultra-condensed",
-		"expanded",
-		"semi-expanded",
-		"extra-expanded",
-		"ultra-expanded"
-	];
-
-	const cssFontSizeKeywords = [
-		"xx-small",
-		"x-small",
-		"small",
-		"medium",
-		"large",
-		"x-large",
-		"xx-large",
-		"larger",
-		"smaller"
-	];
-
-	const cssListHelpers = {
-		splitBySpaces,
-		split,
-		splitByCommas
-	};
-
-	const helpers = {
-		isSize
-	};
-
-	const errorPrefix = "[parse-css-font] ";
+const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
+const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
+
+const globalKeywords = [
+	"inherit",
+	"initial",
+	"unset"
+];
+
+const systemFontKeywords = [
+	"caption",
+	"icon",
+	"menu",
+	"message-box",
+	"small-caption",
+	"status-bar"
+];
+
+const fontWeightKeywords = [
+	"normal",
+	"bold",
+	"bolder",
+	"lighter",
+	"100",
+	"200",
+	"300",
+	"400",
+	"500",
+	"600",
+	"700",
+	"800",
+	"900"
+];
+
+const fontStyleKeywords = [
+	"normal",
+	"italic",
+	"oblique"
+];
+
+const fontStretchKeywords = [
+	"normal",
+	"condensed",
+	"semi-condensed",
+	"extra-condensed",
+	"ultra-condensed",
+	"expanded",
+	"semi-expanded",
+	"extra-expanded",
+	"ultra-expanded"
+];
+
+const cssFontSizeKeywords = [
+	"xx-small",
+	"x-small",
+	"small",
+	"medium",
+	"large",
+	"x-large",
+	"xx-large",
+	"larger",
+	"smaller"
+];
+
+const cssListHelpers = {
+	splitBySpaces,
+	split,
+	splitByCommas
+};
+
+const helpers = {
+	isSize
+};
+
+const errorPrefix = "[parse-css-font] ";
+
+export {
+	parse
+};
+
+function parse(value) {
+	if (typeof value !== "string") {
+		throw new TypeError(errorPrefix + "Expected a string.");
+	}
+	if (value === "") {
+		throw error("Cannot parse an empty string.");
+	}
+	if (systemFontKeywords.indexOf(value) !== -1) {
+		return { system: value };
+	}
 
-	return {
-		parse
+	const font = {
+		lineHeight: "normal",
+		stretch: "normal",
+		style: "normal",
+		variant: "normal",
+		weight: "normal",
 	};
 
-	function parse(value) {
-		if (typeof value !== "string") {
-			throw new TypeError(errorPrefix + "Expected a string.");
-		}
-		if (value === "") {
-			throw error("Cannot parse an empty string.");
-		}
-		if (systemFontKeywords.indexOf(value) !== -1) {
-			return { system: value };
+	let isLocked = false;
+	const tokens = cssListHelpers.splitBySpaces(value);
+	let token = tokens.shift();
+	for (; token; token = tokens.shift()) {
+
+		if (token === "normal" || globalKeywords.indexOf(token) !== -1) {
+			["style", "variant", "weight", "stretch"].forEach((prop) => {
+				font[prop] = token;
+			});
+			isLocked = true;
+			continue;
 		}
 
-		const font = {
-			lineHeight: "normal",
-			stretch: "normal",
-			style: "normal",
-			variant: "normal",
-			weight: "normal",
-		};
-
-		let isLocked = false;
-		const tokens = cssListHelpers.splitBySpaces(value);
-		let token = tokens.shift();
-		for (; token; token = tokens.shift()) {
-
-			if (token === "normal" || globalKeywords.indexOf(token) !== -1) {
-				["style", "variant", "weight", "stretch"].forEach((prop) => {
-					font[prop] = token;
-				});
-				isLocked = true;
-				continue;
-			}
-
-			if (fontWeightKeywords.indexOf(token) !== -1) {
-				if (isLocked) {
-					continue;
-				}
-				font.weight = token;
+		if (fontWeightKeywords.indexOf(token) !== -1) {
+			if (isLocked) {
 				continue;
 			}
+			font.weight = token;
+			continue;
+		}
 
-			if (fontStyleKeywords.indexOf(token) !== -1) {
-				if (isLocked) {
-					continue;
-				}
-				font.style = token;
+		if (fontStyleKeywords.indexOf(token) !== -1) {
+			if (isLocked) {
 				continue;
 			}
+			font.style = token;
+			continue;
+		}
 
-			if (fontStretchKeywords.indexOf(token) !== -1) {
-				if (isLocked) {
-					continue;
-				}
-				font.stretch = token;
+		if (fontStretchKeywords.indexOf(token) !== -1) {
+			if (isLocked) {
 				continue;
 			}
+			font.stretch = token;
+			continue;
+		}
 
-			if (helpers.isSize(token)) {
-				const parts = cssListHelpers.split(token, ["/"]);
-				font.size = parts[0];
-				if (parts[1]) {
-					font.lineHeight = parseLineHeight(parts[1]);
-				} else if (tokens[0] === "/") {
-					tokens.shift();
-					font.lineHeight = parseLineHeight(tokens.shift());
-				}
-				if (!tokens.length) {
-					throw error("Missing required font-family.");
-				}
-				font.family = cssListHelpers.splitByCommas(tokens.join(" ")).map(removeQuotes);
-				return font;
+		if (helpers.isSize(token)) {
+			const parts = cssListHelpers.split(token, ["/"]);
+			font.size = parts[0];
+			if (parts[1]) {
+				font.lineHeight = parseLineHeight(parts[1]);
+			} else if (tokens[0] === "/") {
+				tokens.shift();
+				font.lineHeight = parseLineHeight(tokens.shift());
 			}
-
-			if (font.variant !== "normal") {
-				throw error("Unknown or unsupported font token: " + font.variant);
+			if (!tokens.length) {
+				throw error("Missing required font-family.");
 			}
+			font.family = cssListHelpers.splitByCommas(tokens.join(" ")).map(removeQuotes);
+			return font;
+		}
 
-			if (isLocked) {
-				continue;
-			}
-			font.variant = token;
+		if (font.variant !== "normal") {
+			throw error("Unknown or unsupported font token: " + font.variant);
 		}
 
-		throw error("Missing required font-size.");
+		if (isLocked) {
+			continue;
+		}
+		font.variant = token;
 	}
 
-	function error(message) {
-		return new Error(errorPrefix + message);
-	}
+	throw error("Missing required font-size.");
+}
 
-	function parseLineHeight(value) {
-		const parsed = parseFloat(value);
-		if (parsed.toString() === value) {
-			return parsed;
-		}
-		return value;
+function error(message) {
+	return new Error(errorPrefix + message);
+}
+
+function parseLineHeight(value) {
+	const parsed = parseFloat(value);
+	if (parsed.toString() === value) {
+		return parsed;
 	}
+	return value;
+}
 
+/**
+ * Splits a CSS declaration value (shorthand) using provided separators
+ * as the delimiters.
+ */
+function split(
 	/**
-	 * Splits a CSS declaration value (shorthand) using provided separators
-	 * as the delimiters.
+	 * A CSS declaration value (shorthand).
 	 */
-	function split(
-		/**
-		 * A CSS declaration value (shorthand).
-		 */
-		value,
-		/**
-		 * Any number of separator characters used for splitting.
-		 */
-		separators,
-		{
-			last = false,
-		} = {},
-	) {
-		if (typeof value !== "string") {
-			throw new TypeError("expected a string");
-		}
-		if (!Array.isArray(separators)) {
-			throw new TypeError("expected a string array of separators");
-		}
-		if (typeof last !== "boolean") {
-			throw new TypeError("expected a Boolean value for options.last");
-		}
-		const array = [];
-		let current = "";
-		let splitMe = false;
-
-		let func = 0;
-		let quote = false;
-		let escape = false;
-
-		for (const char of value) {
-
-			if (quote) {
-				if (escape) {
-					escape = false;
-				} else if (char === "\\") {
-					escape = true;
-				} else if (char === quote) {
-					quote = false;
-				}
-			} else if (char === "\"" || char === "'") {
-				quote = char;
-			} else if (char === "(") {
-				func += 1;
-			} else if (char === ")") {
-				if (func > 0) {
-					func -= 1;
-				}
-			} else if (func === 0) {
-				if (separators.indexOf(char) !== -1) {
-					splitMe = true;
-				}
+	value,
+	/**
+	 * Any number of separator characters used for splitting.
+	 */
+	separators,
+	{
+		last = false,
+	} = {},
+) {
+	if (typeof value !== "string") {
+		throw new TypeError("expected a string");
+	}
+	if (!Array.isArray(separators)) {
+		throw new TypeError("expected a string array of separators");
+	}
+	if (typeof last !== "boolean") {
+		throw new TypeError("expected a Boolean value for options.last");
+	}
+	const array = [];
+	let current = "";
+	let splitMe = false;
+
+	let func = 0;
+	let quote = false;
+	let escape = false;
+
+	for (const char of value) {
+
+		if (quote) {
+			if (escape) {
+				escape = false;
+			} else if (char === "\\") {
+				escape = true;
+			} else if (char === quote) {
+				quote = false;
 			}
-
-			if (splitMe) {
-				if (current !== "") {
-					array.push(current.trim());
-				}
-				current = "";
-				splitMe = false;
-			} else {
-				current += char;
+		} else if (char === "\"" || char === "'") {
+			quote = char;
+		} else if (char === "(") {
+			func += 1;
+		} else if (char === ")") {
+			if (func > 0) {
+				func -= 1;
+			}
+		} else if (func === 0) {
+			if (separators.indexOf(char) !== -1) {
+				splitMe = true;
 			}
 		}
 
-		if (last || current !== "") {
-			array.push(current.trim());
+		if (splitMe) {
+			if (current !== "") {
+				array.push(current.trim());
+			}
+			current = "";
+			splitMe = false;
+		} else {
+			current += char;
 		}
-		return array;
 	}
 
-	/**
-	 * Splits a CSS declaration value (shorthand) using whitespace characters
-	 * as the delimiters.
-	 */
-	function splitBySpaces(
-		/**
-		 * A CSS declaration value (shorthand).
-		 */
-		value,
-	) {
-		const spaces = [" ", "\n", "\t"];
-		return split(value, spaces);
+	if (last || current !== "") {
+		array.push(current.trim());
 	}
+	return array;
+}
 
+/**
+ * Splits a CSS declaration value (shorthand) using whitespace characters
+ * as the delimiters.
+ */
+function splitBySpaces(
 	/**
-	 * Splits a CSS declaration value (shorthand) using commas as the delimiters.
+	 * A CSS declaration value (shorthand).
 	 */
-	function splitByCommas(
-		/**
-		 * A CSS declaration value (shorthand).
-		 */
-		value,
-	) {
-		const comma = ",";
-		return split(value, [comma], { last: true });
-	}
-
-	function isSize(value) {
-		return !isNaN(parseFloat(value))
-			|| value.indexOf("/") !== -1
-			|| cssFontSizeKeywords.indexOf(value) !== -1;
-	}
-
-	function removeQuotes(string) {
-		if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
-			string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
-		} else {
-			string = string.replace(REGEXP_DOUBLE_QUOTES_STRING, "$1");
-		}
-		return string.trim();
+	value,
+) {
+	const spaces = [" ", "\n", "\t"];
+	return split(value, spaces);
+}
+
+/**
+ * Splits a CSS declaration value (shorthand) using commas as the delimiters.
+ */
+function splitByCommas(
+	/**
+	 * A CSS declaration value (shorthand).
+	 */
+	value,
+) {
+	const comma = ",";
+	return split(value, [comma], { last: true });
+}
+
+function isSize(value) {
+	return !isNaN(parseFloat(value))
+		|| value.indexOf("/") !== -1
+		|| cssFontSizeKeywords.indexOf(value) !== -1;
+}
+
+function removeQuotes(string) {
+	if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
+		string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
+	} else {
+		string = string.replace(REGEXP_DOUBLE_QUOTES_STRING, "$1");
 	}
-
-})();
+	return string.trim();
+}

+ 365 - 369
lib/single-file/vendor/css-media-query-parser.js

@@ -46,433 +46,429 @@
  * THE SOFTWARE.
 */
 
-this.singlefile.lib.vendor.mediaQueryParser = this.singlefile.lib.vendor.mediaQueryParser || (() => {
-
-	/**
-	 * Parses a media feature expression, e.g. `max-width: 10px`, `(color)`
-	 *
-	 * @param {string} string - the source expression string, can be inside parens
-	 * @param {Number} index - the index of `string` in the overall input
-	 *
-	 * @return {Array} an array of Nodes, the first element being a media feature,
-	 *    the second - its value (may be missing)
-	 */
-
-	function parseMediaFeature(string, index = 0) {
-		const modesEntered = [{
-			mode: "normal",
-			character: null,
-		}];
-		const result = [];
-		let lastModeIndex = 0, mediaFeature = "", colon = null, mediaFeatureValue = null, indexLocal = index;
-
-		let stringNormalized = string;
-		// Strip trailing parens (if any), and correct the starting index
-		if (string[0] === "(" && string[string.length - 1] === ")") {
-			stringNormalized = string.substring(1, string.length - 1);
-			indexLocal++;
-		}
+/**
+ * Parses a media feature expression, e.g. `max-width: 10px`, `(color)`
+ *
+ * @param {string} string - the source expression string, can be inside parens
+ * @param {Number} index - the index of `string` in the overall input
+ *
+ * @return {Array} an array of Nodes, the first element being a media feature,
+ *    the second - its value (may be missing)
+ */
 
-		for (let i = 0; i < stringNormalized.length; i++) {
-			const character = stringNormalized[i];
-
-			// If entering/exiting a string
-			if (character === "'" || character === "\"") {
-				if (modesEntered[lastModeIndex].isCalculationEnabled === true) {
-					modesEntered.push({
-						mode: "string",
-						isCalculationEnabled: false,
-						character,
-					});
-					lastModeIndex++;
-				} else if (modesEntered[lastModeIndex].mode === "string" &&
-					modesEntered[lastModeIndex].character === character &&
-					stringNormalized[i - 1] !== "\\"
-				) {
-					modesEntered.pop();
-					lastModeIndex--;
-				}
-			}
+function parseMediaFeature(string, index = 0) {
+	const modesEntered = [{
+		mode: "normal",
+		character: null,
+	}];
+	const result = [];
+	let lastModeIndex = 0, mediaFeature = "", colon = null, mediaFeatureValue = null, indexLocal = index;
+
+	let stringNormalized = string;
+	// Strip trailing parens (if any), and correct the starting index
+	if (string[0] === "(" && string[string.length - 1] === ")") {
+		stringNormalized = string.substring(1, string.length - 1);
+		indexLocal++;
+	}
 
-			// If entering/exiting interpolation
-			if (character === "{") {
+	for (let i = 0; i < stringNormalized.length; i++) {
+		const character = stringNormalized[i];
+
+		// If entering/exiting a string
+		if (character === "'" || character === "\"") {
+			if (modesEntered[lastModeIndex].isCalculationEnabled === true) {
 				modesEntered.push({
-					mode: "interpolation",
-					isCalculationEnabled: true,
+					mode: "string",
+					isCalculationEnabled: false,
+					character,
 				});
 				lastModeIndex++;
-			} else if (character === "}") {
+			} else if (modesEntered[lastModeIndex].mode === "string" &&
+				modesEntered[lastModeIndex].character === character &&
+				stringNormalized[i - 1] !== "\\"
+			) {
 				modesEntered.pop();
 				lastModeIndex--;
 			}
+		}
 
-			// If a : is met outside of a string, function call or interpolation, than
-			// this : separates a media feature and a value
-			if (modesEntered[lastModeIndex].mode === "normal" && character === ":") {
-				const mediaFeatureValueStr = stringNormalized.substring(i + 1);
-				mediaFeatureValue = {
-					type: "value",
-					before: /^(\s*)/.exec(mediaFeatureValueStr)[1],
-					after: /(\s*)$/.exec(mediaFeatureValueStr)[1],
-					value: mediaFeatureValueStr.trim(),
-				};
-				// +1 for the colon
-				mediaFeatureValue.sourceIndex =
-					mediaFeatureValue.before.length + i + 1 + indexLocal;
-				colon = {
-					type: "colon",
-					sourceIndex: i + indexLocal,
-					after: mediaFeatureValue.before,
-					value: ":", // for consistency only
-				};
-				break;
-			}
+		// If entering/exiting interpolation
+		if (character === "{") {
+			modesEntered.push({
+				mode: "interpolation",
+				isCalculationEnabled: true,
+			});
+			lastModeIndex++;
+		} else if (character === "}") {
+			modesEntered.pop();
+			lastModeIndex--;
+		}
 
-			mediaFeature += character;
+		// If a : is met outside of a string, function call or interpolation, than
+		// this : separates a media feature and a value
+		if (modesEntered[lastModeIndex].mode === "normal" && character === ":") {
+			const mediaFeatureValueStr = stringNormalized.substring(i + 1);
+			mediaFeatureValue = {
+				type: "value",
+				before: /^(\s*)/.exec(mediaFeatureValueStr)[1],
+				after: /(\s*)$/.exec(mediaFeatureValueStr)[1],
+				value: mediaFeatureValueStr.trim(),
+			};
+			// +1 for the colon
+			mediaFeatureValue.sourceIndex =
+				mediaFeatureValue.before.length + i + 1 + indexLocal;
+			colon = {
+				type: "colon",
+				sourceIndex: i + indexLocal,
+				after: mediaFeatureValue.before,
+				value: ":", // for consistency only
+			};
+			break;
 		}
 
-		// Forming a media feature node
-		mediaFeature = {
-			type: "media-feature",
-			before: /^(\s*)/.exec(mediaFeature)[1],
-			after: /(\s*)$/.exec(mediaFeature)[1],
-			value: mediaFeature.trim(),
-		};
-		mediaFeature.sourceIndex = mediaFeature.before.length + indexLocal;
-		result.push(mediaFeature);
+		mediaFeature += character;
+	}
 
-		if (colon !== null) {
-			colon.before = mediaFeature.after;
-			result.push(colon);
-		}
+	// Forming a media feature node
+	mediaFeature = {
+		type: "media-feature",
+		before: /^(\s*)/.exec(mediaFeature)[1],
+		after: /(\s*)$/.exec(mediaFeature)[1],
+		value: mediaFeature.trim(),
+	};
+	mediaFeature.sourceIndex = mediaFeature.before.length + indexLocal;
+	result.push(mediaFeature);
 
-		if (mediaFeatureValue !== null) {
-			result.push(mediaFeatureValue);
-		}
+	if (colon !== null) {
+		colon.before = mediaFeature.after;
+		result.push(colon);
+	}
 
-		return result;
+	if (mediaFeatureValue !== null) {
+		result.push(mediaFeatureValue);
 	}
 
-	/**
-	 * Parses a media query, e.g. `screen and (color)`, `only tv`
-	 *
-	 * @param {string} string - the source media query string
-	 * @param {Number} index - the index of `string` in the overall input
-	 *
-	 * @return {Array} an array of Nodes and Containers
-	 */
-
-	function parseMediaQuery(string, index = 0) {
-		const result = [];
-
-		// How many times the parser entered parens/curly braces
-		let localLevel = 0;
-		// Has any keyword, media type, media feature expression or interpolation
-		// ('element' hereafter) started
-		let insideSomeValue = false, node;
-
-		function resetNode() {
-			return {
-				before: "",
-				after: "",
-				value: "",
-			};
-		}
+	return result;
+}
 
-		node = resetNode();
+/**
+ * Parses a media query, e.g. `screen and (color)`, `only tv`
+ *
+ * @param {string} string - the source media query string
+ * @param {Number} index - the index of `string` in the overall input
+ *
+ * @return {Array} an array of Nodes and Containers
+ */
 
-		for (let i = 0; i < string.length; i++) {
-			const character = string[i];
-			// If not yet entered any element
-			if (!insideSomeValue) {
-				if (character.search(/\s/) !== -1) {
-					// A whitespace
-					// Don't form 'after' yet; will do it later
-					node.before += character;
-				} else {
-					// Not a whitespace - entering an element
-					// Expression start
-					if (character === "(") {
-						node.type = "media-feature-expression";
-						localLevel++;
-					}
-					node.value = character;
-					node.sourceIndex = index + i;
-					insideSomeValue = true;
-				}
+function parseMediaQuery(string, index = 0) {
+	const result = [];
+
+	// How many times the parser entered parens/curly braces
+	let localLevel = 0;
+	// Has any keyword, media type, media feature expression or interpolation
+	// ('element' hereafter) started
+	let insideSomeValue = false, node;
+
+	function resetNode() {
+		return {
+			before: "",
+			after: "",
+			value: "",
+		};
+	}
+
+	node = resetNode();
+
+	for (let i = 0; i < string.length; i++) {
+		const character = string[i];
+		// If not yet entered any element
+		if (!insideSomeValue) {
+			if (character.search(/\s/) !== -1) {
+				// A whitespace
+				// Don't form 'after' yet; will do it later
+				node.before += character;
 			} else {
-				// Already in the middle of some element
-				node.value += character;
-
-				// Here parens just increase localLevel and don't trigger a start of
-				// a media feature expression (since they can't be nested)
-				// Interpolation start
-				if (character === "{" || character === "(") { localLevel++; }
-				// Interpolation/function call/media feature expression end
-				if (character === ")" || character === "}") { localLevel--; }
+				// Not a whitespace - entering an element
+				// Expression start
+				if (character === "(") {
+					node.type = "media-feature-expression";
+					localLevel++;
+				}
+				node.value = character;
+				node.sourceIndex = index + i;
+				insideSomeValue = true;
+			}
+		} else {
+			// Already in the middle of some element
+			node.value += character;
+
+			// Here parens just increase localLevel and don't trigger a start of
+			// a media feature expression (since they can't be nested)
+			// Interpolation start
+			if (character === "{" || character === "(") { localLevel++; }
+			// Interpolation/function call/media feature expression end
+			if (character === ")" || character === "}") { localLevel--; }
+		}
+
+		// If exited all parens/curlies and the next symbol
+		if (insideSomeValue && localLevel === 0 &&
+			(character === ")" || i === string.length - 1 ||
+				string[i + 1].search(/\s/) !== -1)
+		) {
+			if (["not", "only", "and"].indexOf(node.value) !== -1) {
+				node.type = "keyword";
+			}
+			// if it's an expression, parse its contents
+			if (node.type === "media-feature-expression") {
+				node.nodes = parseMediaFeature(node.value, node.sourceIndex);
 			}
+			result.push(Array.isArray(node.nodes) ?
+				new Container(node) : new Node(node));
+			node = resetNode();
+			insideSomeValue = false;
+		}
+	}
 
-			// If exited all parens/curlies and the next symbol
-			if (insideSomeValue && localLevel === 0 &&
-				(character === ")" || i === string.length - 1 ||
-					string[i + 1].search(/\s/) !== -1)
-			) {
-				if (["not", "only", "and"].indexOf(node.value) !== -1) {
+	// Now process the result array - to specify undefined types of the nodes
+	// and specify the `after` prop
+	for (let i = 0; i < result.length; i++) {
+		node = result[i];
+		if (i > 0) { result[i - 1].after = node.before; }
+
+		// Node types. Might not be set because contains interpolation/function
+		// calls or fully consists of them
+		if (node.type === undefined) {
+			if (i > 0) {
+				// only `and` can follow an expression
+				if (result[i - 1].type === "media-feature-expression") {
 					node.type = "keyword";
+					continue;
 				}
-				// if it's an expression, parse its contents
-				if (node.type === "media-feature-expression") {
-					node.nodes = parseMediaFeature(node.value, node.sourceIndex);
+				// Anything after 'only|not' is a media type
+				if (result[i - 1].value === "not" || result[i - 1].value === "only") {
+					node.type = "media-type";
+					continue;
+				}
+				// Anything after 'and' is an expression
+				if (result[i - 1].value === "and") {
+					node.type = "media-feature-expression";
+					continue;
 				}
-				result.push(Array.isArray(node.nodes) ?
-					new Container(node) : new Node(node));
-				node = resetNode();
-				insideSomeValue = false;
-			}
-		}
 
-		// Now process the result array - to specify undefined types of the nodes
-		// and specify the `after` prop
-		for (let i = 0; i < result.length; i++) {
-			node = result[i];
-			if (i > 0) { result[i - 1].after = node.before; }
-
-			// Node types. Might not be set because contains interpolation/function
-			// calls or fully consists of them
-			if (node.type === undefined) {
-				if (i > 0) {
-					// only `and` can follow an expression
-					if (result[i - 1].type === "media-feature-expression") {
-						node.type = "keyword";
-						continue;
-					}
-					// Anything after 'only|not' is a media type
-					if (result[i - 1].value === "not" || result[i - 1].value === "only") {
-						node.type = "media-type";
-						continue;
-					}
-					// Anything after 'and' is an expression
-					if (result[i - 1].value === "and") {
+				if (result[i - 1].type === "media-type") {
+					// if it is the last element - it might be an expression
+					// or 'and' depending on what is after it
+					if (!result[i + 1]) {
 						node.type = "media-feature-expression";
-						continue;
+					} else {
+						node.type = result[i + 1].type === "media-feature-expression" ?
+							"keyword" : "media-feature-expression";
 					}
+				}
+			}
 
-					if (result[i - 1].type === "media-type") {
-						// if it is the last element - it might be an expression
-						// or 'and' depending on what is after it
-						if (!result[i + 1]) {
-							node.type = "media-feature-expression";
-						} else {
-							node.type = result[i + 1].type === "media-feature-expression" ?
-								"keyword" : "media-feature-expression";
-						}
-					}
+			if (i === 0) {
+				// `screen`, `fn( ... )`, `#{ ... }`. Not an expression, since then
+				// its type would have been set by now
+				if (!result[i + 1]) {
+					node.type = "media-type";
+					continue;
 				}
 
-				if (i === 0) {
-					// `screen`, `fn( ... )`, `#{ ... }`. Not an expression, since then
-					// its type would have been set by now
-					if (!result[i + 1]) {
+				// `screen and` or `#{...} (max-width: 10px)`
+				if (result[i + 1] &&
+					(result[i + 1].type === "media-feature-expression" ||
+						result[i + 1].type === "keyword")
+				) {
+					node.type = "media-type";
+					continue;
+				}
+				if (result[i + 2]) {
+					// `screen and (color) ...`
+					if (result[i + 2].type === "media-feature-expression") {
 						node.type = "media-type";
+						result[i + 1].type = "keyword";
 						continue;
 					}
-
-					// `screen and` or `#{...} (max-width: 10px)`
-					if (result[i + 1] &&
-						(result[i + 1].type === "media-feature-expression" ||
-							result[i + 1].type === "keyword")
-					) {
-						node.type = "media-type";
+					// `only screen and ...`
+					if (result[i + 2].type === "keyword") {
+						node.type = "keyword";
+						result[i + 1].type = "media-type";
 						continue;
 					}
-					if (result[i + 2]) {
-						// `screen and (color) ...`
-						if (result[i + 2].type === "media-feature-expression") {
-							node.type = "media-type";
-							result[i + 1].type = "keyword";
-							continue;
-						}
-						// `only screen and ...`
-						if (result[i + 2].type === "keyword") {
-							node.type = "keyword";
-							result[i + 1].type = "media-type";
-							continue;
-						}
-					}
-					if (result[i + 3]) {
-						// `screen and (color) ...`
-						if (result[i + 3].type === "media-feature-expression") {
-							node.type = "keyword";
-							result[i + 1].type = "media-type";
-							result[i + 2].type = "keyword";
-							continue;
-						}
+				}
+				if (result[i + 3]) {
+					// `screen and (color) ...`
+					if (result[i + 3].type === "media-feature-expression") {
+						node.type = "keyword";
+						result[i + 1].type = "media-type";
+						result[i + 2].type = "keyword";
+						continue;
 					}
 				}
 			}
 		}
-		return result;
 	}
+	return result;
+}
+
+/**
+ * Parses a media query list. Takes a possible `url()` at the start into
+ * account, and divides the list into media queries that are parsed separately
+ *
+ * @param {string} string - the source media query list string
+ *
+ * @return {Array} an array of Nodes/Containers
+ */
 
-	/**
-	 * Parses a media query list. Takes a possible `url()` at the start into
-	 * account, and divides the list into media queries that are parsed separately
-	 *
-	 * @param {string} string - the source media query list string
-	 *
-	 * @return {Array} an array of Nodes/Containers
-	 */
-
-	function parseMediaList(string) {
-		const result = [];
-		let interimIndex = 0, levelLocal = 0;
-
-		// Check for a `url(...)` part (if it is contents of an @import rule)
-		const doesHaveUrl = /^(\s*)url\s*\(/.exec(string);
-		if (doesHaveUrl !== null) {
-			let i = doesHaveUrl[0].length;
-			let parenthesesLv = 1;
-			while (parenthesesLv > 0) {
-				const character = string[i];
-				if (character === "(") { parenthesesLv++; }
-				if (character === ")") { parenthesesLv--; }
-				i++;
-			}
-			result.unshift(new Node({
-				type: "url",
-				value: string.substring(0, i).trim(),
-				sourceIndex: doesHaveUrl[1].length,
-				before: doesHaveUrl[1],
-				after: /^(\s*)/.exec(string.substring(i))[1],
-			}));
-			interimIndex = i;
-		}
+function parseMediaList(string) {
+	const result = [];
+	let interimIndex = 0, levelLocal = 0;
 
-		// Start processing the media query list
-		for (let i = interimIndex; i < string.length; i++) {
+	// Check for a `url(...)` part (if it is contents of an @import rule)
+	const doesHaveUrl = /^(\s*)url\s*\(/.exec(string);
+	if (doesHaveUrl !== null) {
+		let i = doesHaveUrl[0].length;
+		let parenthesesLv = 1;
+		while (parenthesesLv > 0) {
 			const character = string[i];
-
-			// Dividing the media query list into comma-separated media queries
-			// Only count commas that are outside of any parens
-			// (i.e., not part of function call params list, etc.)
-			if (character === "(") { levelLocal++; }
-			if (character === ")") { levelLocal--; }
-			if (levelLocal === 0 && character === ",") {
-				const mediaQueryString = string.substring(interimIndex, i);
-				const spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
-				result.push(new Container({
-					type: "media-query",
-					value: mediaQueryString.trim(),
-					sourceIndex: interimIndex + spaceBefore.length,
-					nodes: parseMediaQuery(mediaQueryString, interimIndex),
-					before: spaceBefore,
-					after: /(\s*)$/.exec(mediaQueryString)[1],
-				}));
-				interimIndex = i + 1;
-			}
+			if (character === "(") { parenthesesLv++; }
+			if (character === ")") { parenthesesLv--; }
+			i++;
 		}
-
-		const mediaQueryString = string.substring(interimIndex);
-		const spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
-		result.push(new Container({
-			type: "media-query",
-			value: mediaQueryString.trim(),
-			sourceIndex: interimIndex + spaceBefore.length,
-			nodes: parseMediaQuery(mediaQueryString, interimIndex),
-			before: spaceBefore,
-			after: /(\s*)$/.exec(mediaQueryString)[1],
+		result.unshift(new Node({
+			type: "url",
+			value: string.substring(0, i).trim(),
+			sourceIndex: doesHaveUrl[1].length,
+			before: doesHaveUrl[1],
+			after: /^(\s*)/.exec(string.substring(i))[1],
 		}));
+		interimIndex = i;
+	}
 
-		return result;
+	// Start processing the media query list
+	for (let i = interimIndex; i < string.length; i++) {
+		const character = string[i];
+
+		// Dividing the media query list into comma-separated media queries
+		// Only count commas that are outside of any parens
+		// (i.e., not part of function call params list, etc.)
+		if (character === "(") { levelLocal++; }
+		if (character === ")") { levelLocal--; }
+		if (levelLocal === 0 && character === ",") {
+			const mediaQueryString = string.substring(interimIndex, i);
+			const spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
+			result.push(new Container({
+				type: "media-query",
+				value: mediaQueryString.trim(),
+				sourceIndex: interimIndex + spaceBefore.length,
+				nodes: parseMediaQuery(mediaQueryString, interimIndex),
+				before: spaceBefore,
+				after: /(\s*)$/.exec(mediaQueryString)[1],
+			}));
+			interimIndex = i + 1;
+		}
 	}
 
-	function Container(opts) {
-		this.constructor(opts);
+	const mediaQueryString = string.substring(interimIndex);
+	const spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
+	result.push(new Container({
+		type: "media-query",
+		value: mediaQueryString.trim(),
+		sourceIndex: interimIndex + spaceBefore.length,
+		nodes: parseMediaQuery(mediaQueryString, interimIndex),
+		before: spaceBefore,
+		after: /(\s*)$/.exec(mediaQueryString)[1],
+	}));
 
-		this.nodes = opts.nodes;
+	return result;
+}
 
-		if (this.after === undefined) {
-			this.after = this.nodes.length > 0 ?
-				this.nodes[this.nodes.length - 1].after : "";
-		}
+function Container(opts) {
+	this.constructor(opts);
 
-		if (this.before === undefined) {
-			this.before = this.nodes.length > 0 ?
-				this.nodes[0].before : "";
-		}
+	this.nodes = opts.nodes;
 
-		if (this.sourceIndex === undefined) {
-			this.sourceIndex = this.before.length;
-		}
+	if (this.after === undefined) {
+		this.after = this.nodes.length > 0 ?
+			this.nodes[this.nodes.length - 1].after : "";
+	}
 
-		this.nodes.forEach(node => {
-			node.parent = this; // eslint-disable-line no-param-reassign
-		});
+	if (this.before === undefined) {
+		this.before = this.nodes.length > 0 ?
+			this.nodes[0].before : "";
 	}
 
-	Container.prototype = Object.create(Node.prototype);
-	Container.constructor = Node;
-
-	/**
-	 * Iterate over descendant nodes of the node
-	 *
-	 * @param {RegExp|string} filter - Optional. Only nodes with node.type that
-	 *    satisfies the filter will be traversed over
-	 * @param {function} cb - callback to call on each node. Takes these params:
-	 *    node - the node being processed, i - it's index, nodes - the array
-	 *    of all nodes
-	 *    If false is returned, the iteration breaks
-	 *
-	 * @return (boolean) false, if the iteration was broken
-	 */
-	Container.prototype.walk = function walk(filter, cb) {
-		const hasFilter = typeof filter === "string" || filter instanceof RegExp;
-		const callback = hasFilter ? cb : filter;
-		const filterReg = typeof filter === "string" ? new RegExp(filter) : filter;
-
-		for (let i = 0; i < this.nodes.length; i++) {
-			const node = this.nodes[i];
-			const filtered = hasFilter ? filterReg.test(node.type) : true;
-			if (filtered && callback && callback(node, i, this.nodes) === false) {
-				return false;
-			}
-			if (node.nodes && node.walk(filter, cb) === false) { return false; }
-		}
-		return true;
-	};
+	if (this.sourceIndex === undefined) {
+		this.sourceIndex = this.before.length;
+	}
 
-	/**
-	 * Iterate over immediate children of the node
-	 *
-	 * @param {function} cb - callback to call on each node. Takes these params:
-	 *    node - the node being processed, i - it's index, nodes - the array
-	 *    of all nodes
-	 *    If false is returned, the iteration breaks
-	 *
-	 * @return (boolean) false, if the iteration was broken
-	 */
-	Container.prototype.each = function each(cb = () => { }) {
-		for (let i = 0; i < this.nodes.length; i++) {
-			const node = this.nodes[i];
-			if (cb(node, i, this.nodes) === false) { return false; }
+	this.nodes.forEach(node => {
+		node.parent = this; // eslint-disable-line no-param-reassign
+	});
+}
+
+Container.prototype = Object.create(Node.prototype);
+Container.constructor = Node;
+
+/**
+ * Iterate over descendant nodes of the node
+ *
+ * @param {RegExp|string} filter - Optional. Only nodes with node.type that
+ *    satisfies the filter will be traversed over
+ * @param {function} cb - callback to call on each node. Takes these params:
+ *    node - the node being processed, i - it's index, nodes - the array
+ *    of all nodes
+ *    If false is returned, the iteration breaks
+ *
+ * @return (boolean) false, if the iteration was broken
+ */
+Container.prototype.walk = function walk(filter, cb) {
+	const hasFilter = typeof filter === "string" || filter instanceof RegExp;
+	const callback = hasFilter ? cb : filter;
+	const filterReg = typeof filter === "string" ? new RegExp(filter) : filter;
+
+	for (let i = 0; i < this.nodes.length; i++) {
+		const node = this.nodes[i];
+		const filtered = hasFilter ? filterReg.test(node.type) : true;
+		if (filtered && callback && callback(node, i, this.nodes) === false) {
+			return false;
 		}
-		return true;
-	};
-
-	/**
-	 * A very generic node. Pretty much any element of a media query
-	 */
-
-	function Node(opts) {
-		this.after = opts.after;
-		this.before = opts.before;
-		this.type = opts.type;
-		this.value = opts.value;
-		this.sourceIndex = opts.sourceIndex;
+		if (node.nodes && node.walk(filter, cb) === false) { return false; }
+	}
+	return true;
+};
+
+/**
+ * Iterate over immediate children of the node
+ *
+ * @param {function} cb - callback to call on each node. Takes these params:
+ *    node - the node being processed, i - it's index, nodes - the array
+ *    of all nodes
+ *    If false is returned, the iteration breaks
+ *
+ * @return (boolean) false, if the iteration was broken
+ */
+Container.prototype.each = function each(cb = () => { }) {
+	for (let i = 0; i < this.nodes.length; i++) {
+		const node = this.nodes[i];
+		if (cb(node, i, this.nodes) === false) { return false; }
 	}
+	return true;
+};
 
-	return {
-		parseMediaList
-	};
+/**
+ * A very generic node. Pretty much any element of a media query
+ */
 
-})();
+function Node(opts) {
+	this.after = opts.after;
+	this.before = opts.before;
+	this.type = opts.type;
+	this.value = opts.value;
+	this.sourceIndex = opts.sourceIndex;
+}
+
+export {
+	parseMediaList
+};

+ 613 - 617
lib/single-file/vendor/css-minifier.js

@@ -51,739 +51,735 @@
  * by Yahoo! Inc. under the BSD (revised) open source license.
  */
 
-this.singlefile.lib.vendor.cssMinifier = this.singlefile.lib.vendor.cssMinifier || (() => {
-
-	/**
-	 * @type {string} - placeholder prefix
-	 */
-
-	const ___PRESERVED_TOKEN_ = "___PRESERVED_TOKEN_";
-
-	/**
-	 * @typedef {object} options - UglifyCSS options
-	 * @property {number} [maxLineLen=0] - Maximum line length of uglified CSS
-	 * @property {boolean} [expandVars=false] - Expand variables
-	 * @property {boolean} [uglyComments=false] - Removes newlines within preserved comments
-	 * @property {boolean} [cuteComments=false] - Preserves newlines within and around preserved comments
-	 * @property {boolean} [debug=false] - Prints full error stack on error
-	 * @property {string} [output=''] - Output file name
-	 */
-
-	/**
-	 * @type {options} - UglifyCSS options
-	 */
-
-	const defaultOptions = {
-		maxLineLen: 0,
-		expandVars: false,
-		uglyComments: false,
-		cuteComments: false,
-		debug: false,
-		output: ""
-	};
+/**
+ * @type {string} - placeholder prefix
+ */
 
-	const REGEXP_DATA_URI = /url\(\s*(["']?)data:/g;
-	const REGEXP_WHITE_SPACES = /\s+/g;
-	const REGEXP_NEW_LINE = /\n/g;
+const ___PRESERVED_TOKEN_ = "___PRESERVED_TOKEN_";
 
-	/**
-	 * extractDataUrls replaces all data urls with tokens before we start
-	 * compressing, to avoid performance issues running some of the subsequent
-	 * regexes against large strings chunks.
-	 *
-	 * @param {string} css - CSS content
-	 * @param {string[]} preservedTokens - Global array of tokens to preserve
-	 *
-	 * @return {string} Processed CSS
-	 */
+/**
+ * @typedef {object} options - UglifyCSS options
+ * @property {number} [maxLineLen=0] - Maximum line length of uglified CSS
+ * @property {boolean} [expandVars=false] - Expand variables
+ * @property {boolean} [uglyComments=false] - Removes newlines within preserved comments
+ * @property {boolean} [cuteComments=false] - Preserves newlines within and around preserved comments
+ * @property {boolean} [debug=false] - Prints full error stack on error
+ * @property {string} [output=''] - Output file name
+ */
 
-	function extractDataUrls(css, preservedTokens) {
+/**
+ * @type {options} - UglifyCSS options
+ */
 
-		// Leave data urls alone to increase parse performance.
-		const pattern = REGEXP_DATA_URI;
-		const maxIndex = css.length - 1;
-		const sb = [];
+const defaultOptions = {
+	maxLineLen: 0,
+	expandVars: false,
+	uglyComments: false,
+	cuteComments: false,
+	debug: false,
+	output: ""
+};
 
-		let appendIndex = 0, match;
+const REGEXP_DATA_URI = /url\(\s*(["']?)data:/g;
+const REGEXP_WHITE_SPACES = /\s+/g;
+const REGEXP_NEW_LINE = /\n/g;
 
-		// Since we need to account for non-base64 data urls, we need to handle
-		// ' and ) being part of the data string. Hence switching to indexOf,
-		// to determine whether or not we have matching string terminators and
-		// handling sb appends directly, instead of using matcher.append* methods.
+/**
+ * extractDataUrls replaces all data urls with tokens before we start
+ * compressing, to avoid performance issues running some of the subsequent
+ * regexes against large strings chunks.
+ *
+ * @param {string} css - CSS content
+ * @param {string[]} preservedTokens - Global array of tokens to preserve
+ *
+ * @return {string} Processed CSS
+ */
 
-		while ((match = pattern.exec(css)) !== null) {
+function extractDataUrls(css, preservedTokens) {
 
-			const startIndex = match.index + 4;  // 'url('.length()
-			let terminator = match[1];         // ', " or empty (not quoted)
+	// Leave data urls alone to increase parse performance.
+	const pattern = REGEXP_DATA_URI;
+	const maxIndex = css.length - 1;
+	const sb = [];
 
-			if (terminator.length === 0) {
-				terminator = ")";
-			}
+	let appendIndex = 0, match;
 
-			let foundTerminator = false, endIndex = pattern.lastIndex - 1;
+	// Since we need to account for non-base64 data urls, we need to handle
+	// ' and ) being part of the data string. Hence switching to indexOf,
+	// to determine whether or not we have matching string terminators and
+	// handling sb appends directly, instead of using matcher.append* methods.
 
-			while (foundTerminator === false && endIndex + 1 <= maxIndex && endIndex != -1) {
-				endIndex = css.indexOf(terminator, endIndex + 1);
+	while ((match = pattern.exec(css)) !== null) {
 
-				// endIndex == 0 doesn't really apply here
-				if ((endIndex > 0) && (css.charAt(endIndex - 1) !== "\\")) {
-					foundTerminator = true;
-					if (")" != terminator) {
-						endIndex = css.indexOf(")", endIndex);
-					}
-				}
-			}
+		const startIndex = match.index + 4;  // 'url('.length()
+		let terminator = match[1];         // ', " or empty (not quoted)
+
+		if (terminator.length === 0) {
+			terminator = ")";
+		}
 
-			// Enough searching, start moving stuff over to the buffer
-			sb.push(css.substring(appendIndex, match.index));
+		let foundTerminator = false, endIndex = pattern.lastIndex - 1;
 
-			if (foundTerminator) {
+		while (foundTerminator === false && endIndex + 1 <= maxIndex && endIndex != -1) {
+			endIndex = css.indexOf(terminator, endIndex + 1);
 
-				let token = css.substring(startIndex, endIndex);
-				const parts = token.split(",");
-				if (parts.length > 1 && parts[0].slice(-7) == ";base64") {
-					token = token.replace(REGEXP_WHITE_SPACES, "");
-				} else {
-					token = token.replace(REGEXP_NEW_LINE, " ");
-					token = token.replace(REGEXP_WHITE_SPACES, " ");
-					token = token.replace(REGEXP_PRESERVE_HSLA1, "");
+			// endIndex == 0 doesn't really apply here
+			if ((endIndex > 0) && (css.charAt(endIndex - 1) !== "\\")) {
+				foundTerminator = true;
+				if (")" != terminator) {
+					endIndex = css.indexOf(")", endIndex);
 				}
+			}
+		}
 
-				preservedTokens.push(token);
+		// Enough searching, start moving stuff over to the buffer
+		sb.push(css.substring(appendIndex, match.index));
 
-				const preserver = "url(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___)";
-				sb.push(preserver);
+		if (foundTerminator) {
 
-				appendIndex = endIndex + 1;
+			let token = css.substring(startIndex, endIndex);
+			const parts = token.split(",");
+			if (parts.length > 1 && parts[0].slice(-7) == ";base64") {
+				token = token.replace(REGEXP_WHITE_SPACES, "");
 			} else {
-				// No end terminator found, re-add the whole match. Should we throw/warn here?
-				sb.push(css.substring(match.index, pattern.lastIndex));
-				appendIndex = pattern.lastIndex;
+				token = token.replace(REGEXP_NEW_LINE, " ");
+				token = token.replace(REGEXP_WHITE_SPACES, " ");
+				token = token.replace(REGEXP_PRESERVE_HSLA1, "");
 			}
-		}
 
-		sb.push(css.substring(appendIndex));
+			preservedTokens.push(token);
+
+			const preserver = "url(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___)";
+			sb.push(preserver);
 
-		return sb.join("");
+			appendIndex = endIndex + 1;
+		} else {
+			// No end terminator found, re-add the whole match. Should we throw/warn here?
+			sb.push(css.substring(match.index, pattern.lastIndex));
+			appendIndex = pattern.lastIndex;
+		}
 	}
 
-	const REGEXP_HEX_COLORS = /(=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi;
+	sb.push(css.substring(appendIndex));
 
-	/**
-	 * compressHexColors compresses hex color values of the form #AABBCC to #ABC.
-	 *
-	 * DOES NOT compress CSS ID selectors which match the above pattern (which would
-	 * break things), like #AddressForm { ... }
-	 *
-	 * DOES NOT compress IE filters, which have hex color values (which would break
-	 * things), like chroma(color='#FFFFFF');
-	 *
-	 * DOES NOT compress invalid hex values, like background-color: #aabbccdd
-	 *
-	 * @param {string} css - CSS content
-	 *
-	 * @return {string} Processed CSS
-	 */
+	return sb.join("");
+}
+
+const REGEXP_HEX_COLORS = /(=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi;
+
+/**
+ * compressHexColors compresses hex color values of the form #AABBCC to #ABC.
+ *
+ * DOES NOT compress CSS ID selectors which match the above pattern (which would
+ * break things), like #AddressForm { ... }
+ *
+ * DOES NOT compress IE filters, which have hex color values (which would break
+ * things), like chroma(color='#FFFFFF');
+ *
+ * DOES NOT compress invalid hex values, like background-color: #aabbccdd
+ *
+ * @param {string} css - CSS content
+ *
+ * @return {string} Processed CSS
+ */
 
-	function compressHexColors(css) {
+function compressHexColors(css) {
 
-		// Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
+	// Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
 
-		const pattern = REGEXP_HEX_COLORS;
-		const sb = [];
+	const pattern = REGEXP_HEX_COLORS;
+	const sb = [];
 
-		let index = 0, match;
+	let index = 0, match;
 
-		while ((match = pattern.exec(css)) !== null) {
+	while ((match = pattern.exec(css)) !== null) {
 
-			sb.push(css.substring(index, match.index));
+		sb.push(css.substring(index, match.index));
 
-			const isFilter = match[1];
+		const isFilter = match[1];
 
-			if (isFilter) {
-				// Restore, maintain case, otherwise filter will break
-				sb.push(match[1] + "#" + (match[2] + match[3] + match[4] + match[5] + match[6] + match[7]));
+		if (isFilter) {
+			// Restore, maintain case, otherwise filter will break
+			sb.push(match[1] + "#" + (match[2] + match[3] + match[4] + match[5] + match[6] + match[7]));
+		} else {
+			if (match[2].toLowerCase() == match[3].toLowerCase() &&
+				match[4].toLowerCase() == match[5].toLowerCase() &&
+				match[6].toLowerCase() == match[7].toLowerCase()) {
+
+				// Compress.
+				sb.push("#" + (match[3] + match[5] + match[7]).toLowerCase());
 			} else {
-				if (match[2].toLowerCase() == match[3].toLowerCase() &&
-					match[4].toLowerCase() == match[5].toLowerCase() &&
-					match[6].toLowerCase() == match[7].toLowerCase()) {
-
-					// Compress.
-					sb.push("#" + (match[3] + match[5] + match[7]).toLowerCase());
-				} else {
-					// Non compressible color, restore but lower case.
-					sb.push("#" + (match[2] + match[3] + match[4] + match[5] + match[6] + match[7]).toLowerCase());
-				}
+				// Non compressible color, restore but lower case.
+				sb.push("#" + (match[2] + match[3] + match[4] + match[5] + match[6] + match[7]).toLowerCase());
 			}
-
-			index = pattern.lastIndex = pattern.lastIndex - match[8].length;
 		}
 
-		sb.push(css.substring(index));
-
-		return sb.join("");
+		index = pattern.lastIndex = pattern.lastIndex - match[8].length;
 	}
 
-	const REGEXP_KEYFRAMES = /@[a-z0-9-_]*keyframes\s+[a-z0-9-_]+\s*{/gi;
-	const REGEXP_WHITE_SPACE = /(^\s|\s$)/g;
+	sb.push(css.substring(index));
+
+	return sb.join("");
+}
 
-	/** keyframes preserves 0 followed by unit in keyframes steps
-	 *
-	 * @param {string} content - CSS content
-	 * @param {string[]} preservedTokens - Global array of tokens to preserve
-	 *
-	 * @return {string} Processed CSS
-	 */
+const REGEXP_KEYFRAMES = /@[a-z0-9-_]*keyframes\s+[a-z0-9-_]+\s*{/gi;
+const REGEXP_WHITE_SPACE = /(^\s|\s$)/g;
 
-	function keyframes(content, preservedTokens) {
+/** keyframes preserves 0 followed by unit in keyframes steps
+ *
+ * @param {string} content - CSS content
+ * @param {string[]} preservedTokens - Global array of tokens to preserve
+ *
+ * @return {string} Processed CSS
+ */
 
-		const pattern = REGEXP_KEYFRAMES;
+function keyframes(content, preservedTokens) {
 
-		let index = 0, buffer;
+	const pattern = REGEXP_KEYFRAMES;
 
-		const preserve = (part, i) => {
-			part = part.replace(REGEXP_WHITE_SPACE, "");
-			if (part.charAt(0) === "0") {
-				preservedTokens.push(part);
-				buffer[i] = ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
-			}
-		};
+	let index = 0, buffer;
 
-		while (true) { // eslint-disable-line no-constant-condition
+	const preserve = (part, i) => {
+		part = part.replace(REGEXP_WHITE_SPACE, "");
+		if (part.charAt(0) === "0") {
+			preservedTokens.push(part);
+			buffer[i] = ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
+		}
+	};
 
-			let level = 0;
-			buffer = "";
+	while (true) { // eslint-disable-line no-constant-condition
 
-			let startIndex = content.slice(index).search(pattern);
-			if (startIndex < 0) {
-				break;
-			}
+		let level = 0;
+		buffer = "";
 
-			index += startIndex;
-			startIndex = index;
+		let startIndex = content.slice(index).search(pattern);
+		if (startIndex < 0) {
+			break;
+		}
 
-			const len = content.length;
-			const buffers = [];
+		index += startIndex;
+		startIndex = index;
 
-			for (; index < len; ++index) {
+		const len = content.length;
+		const buffers = [];
 
-				const ch = content.charAt(index);
+		for (; index < len; ++index) {
 
-				if (ch === "{") {
+			const ch = content.charAt(index);
 
-					if (level === 0) {
-						buffers.push(buffer.replace(REGEXP_WHITE_SPACE, ""));
+			if (ch === "{") {
 
-					} else if (level === 1) {
+				if (level === 0) {
+					buffers.push(buffer.replace(REGEXP_WHITE_SPACE, ""));
 
-						buffer = buffer.split(",");
+				} else if (level === 1) {
 
-						buffer.forEach(preserve);
+					buffer = buffer.split(",");
 
-						buffers.push(buffer.join(",").replace(REGEXP_WHITE_SPACE, ""));
-					}
+					buffer.forEach(preserve);
 
-					buffer = "";
-					level += 1;
+					buffers.push(buffer.join(",").replace(REGEXP_WHITE_SPACE, ""));
+				}
 
-				} else if (ch === "}") {
+				buffer = "";
+				level += 1;
 
-					if (level === 2) {
-						buffers.push("{" + buffer.replace(REGEXP_WHITE_SPACE, "") + "}");
-						buffer = "";
+			} else if (ch === "}") {
 
-					} else if (level === 1) {
-						content = content.slice(0, startIndex) +
-							buffers.shift() + "{" +
-							buffers.join("") +
-							content.slice(index);
-						break;
-					}
+				if (level === 2) {
+					buffers.push("{" + buffer.replace(REGEXP_WHITE_SPACE, "") + "}");
+					buffer = "";
 
-					level -= 1;
+				} else if (level === 1) {
+					content = content.slice(0, startIndex) +
+						buffers.shift() + "{" +
+						buffers.join("") +
+						content.slice(index);
+					break;
 				}
 
-				if (level < 0) {
-					break;
+				level -= 1;
+			}
 
-				} else if (ch !== "{" && ch !== "}") {
-					buffer += ch;
-				}
+			if (level < 0) {
+				break;
+
+			} else if (ch !== "{" && ch !== "}") {
+				buffer += ch;
 			}
 		}
-
-		return content;
 	}
 
-	/**
-	 * collectComments collects all comment blocks and return new content with comment placeholders
-	 *
-	 * @param {string} content - CSS content
-	 * @param {string[]} comments - Global array of extracted comments
-	 *
-	 * @return {string} Processed CSS
-	 */
+	return content;
+}
 
-	function collectComments(content, comments) {
+/**
+ * collectComments collects all comment blocks and return new content with comment placeholders
+ *
+ * @param {string} content - CSS content
+ * @param {string[]} comments - Global array of extracted comments
+ *
+ * @return {string} Processed CSS
+ */
 
-		const table = [];
+function collectComments(content, comments) {
 
-		let from = 0, end;
+	const table = [];
 
-		while (true) { // eslint-disable-line no-constant-condition
+	let from = 0, end;
 
-			const start = content.indexOf("/*", from);
+	while (true) { // eslint-disable-line no-constant-condition
 
-			if (start > -1) {
+		const start = content.indexOf("/*", from);
 
-				end = content.indexOf("*/", start + 2);
+		if (start > -1) {
 
-				if (end > -1) {
-					comments.push(content.slice(start + 2, end));
-					table.push(content.slice(from, start));
-					table.push("/*___PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___*/");
-					from = end + 2;
+			end = content.indexOf("*/", start + 2);
 
-				} else {
-					// unterminated comment
-					end = -2;
-					break;
-				}
+			if (end > -1) {
+				comments.push(content.slice(start + 2, end));
+				table.push(content.slice(from, start));
+				table.push("/*___PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___*/");
+				from = end + 2;
 
 			} else {
+				// unterminated comment
+				end = -2;
 				break;
 			}
-		}
 
-		table.push(content.slice(end + 2));
-
-		return table.join("");
+		} else {
+			break;
+		}
 	}
 
-	/**
-	 * processString uglifies a CSS string
-	 *
-	 * @param {string} content - CSS string
-	 * @param {options} options - UglifyCSS options
-	 *
-	 * @return {string} Uglified result
-	 */
-
-	// const REGEXP_EMPTY_RULES = /[^};{/]+\{\}/g;
-	const REGEXP_PRESERVE_STRING = /("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g;
-	const REGEXP_MINIFY_ALPHA = /progid:DXImageTransform.Microsoft.Alpha\(Opacity=/gi;
-	const REGEXP_PRESERVE_TOKEN1 = /\r\n/g;
-	const REGEXP_PRESERVE_TOKEN2 = /[\r\n]/g;
-	const REGEXP_VARIABLES = /@variables\s*\{\s*([^}]+)\s*\}/g;
-	const REGEXP_VARIABLE = /\s*([a-z0-9-]+)\s*:\s*([^;}]+)\s*/gi;
-	const REGEXP_VARIABLE_VALUE = /var\s*\(\s*([^)]+)\s*\)/g;
-	const REGEXP_PRESERVE_CALC = /calc\(([^;}]*)\)/g;
-	const REGEXP_TRIM = /(^\s*|\s*$)/g;
-	const REGEXP_PRESERVE_CALC2 = /\( /g;
-	const REGEXP_PRESERVE_CALC3 = / \)/g;
-	const REGEXP_PRESERVE_MATRIX = /\s*filter:\s*progid:DXImageTransform.Microsoft.Matrix\(([^)]+)\);/g;
-	const REGEXP_REMOVE_SPACES = /(^|\})(([^{:])+:)+([^{]*{)/g;
-	const REGEXP_REMOVE_SPACES2 = /\s+([!{;:>+()\],])/g;
-	const REGEXP_REMOVE_SPACES2_BIS = /([^\\])\s+([}])/g;
-	const REGEXP_RESTORE_SPACE_IMPORTANT = /!important/g;
-	const REGEXP_PSEUDOCLASSCOLON = /___PSEUDOCLASSCOLON___/g;
-	const REGEXP_COLUMN = /:/g;
-	const REGEXP_PRESERVE_ZERO_UNIT = /\s*(animation|animation-delay|animation-duration|transition|transition-delay|transition-duration):\s*([^;}]+)/gi;
-	const REGEXP_PRESERVE_ZERO_UNIT1 = /(^|\D)0?\.?0(m?s)/gi;
-	const REGEXP_PRESERVE_FLEX = /\s*(flex|flex-basis):\s*([^;}]+)/gi;
-	const REGEXP_SPACES = /\s+/;
-	const REGEXP_PRESERVE_HSLA = /(hsla?)\(([^)]+)\)/g;
-	const REGEXP_PRESERVE_HSLA1 = /(^\s+|\s+$)/g;
-	const REGEXP_RETAIN_SPACE_IE6 = /:first-(line|letter)(\{|,)/gi;
-	const REGEXP_CHARSET = /^(.*)(@charset)( "[^"]*";)/gi;
-	const REGEXP_REMOVE_SECOND_CHARSET = /^((\s*)(@charset)( [^;]+;\s*))+/gi;
-	const REGEXP_LOWERCASE_DIRECTIVES = /@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/gi;
-	const REGEXP_LOWERCASE_PSEUDO_ELEMENTS = /:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/gi;
-	const REGEXP_CHARSET2 = /^(.*)(@charset "[^"]*";)/g;
-	const REGEXP_CHARSET3 = /^(\s*@charset [^;]+;\s*)+/g;
-	const REGEXP_LOWERCASE_FUNCTIONS = /:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?any)\(/gi;
-	const REGEXP_LOWERCASE_FUNCTIONS2 = /([:,( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/gi;
-	const REGEXP_NEWLINE1 = /\s*\/\*/g;
-	const REGEXP_NEWLINE2 = /\*\/\s*/g;
-	const REGEXP_RESTORE_SPACE1 = /\band\(/gi;
-	const REGEXP_RESTORE_SPACE2 = /([^:])not\(/gi;
-	const REGEXP_RESTORE_SPACE3 = /\bor\(/gi;
-	const REGEXP_REMOVE_SPACES3 = /([!{}:;>+([,])\s+/g;
-	const REGEXP_REMOVE_SEMI_COLUMNS = /;+\}/g;
-	// const REGEXP_REPLACE_ZERO = /(^|[^.0-9\\])(?:0?\.)?0(?:ex|ch|r?em|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|g?rad|turn|ms|k?Hz|dpi|dpcm|dppx|%)(?![a-z0-9])/gi;
-	const REGEXP_REPLACE_ZERO_DOT = /([0-9])\.0(ex|ch|r?em|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|g?rad|turn|m?s|k?Hz|dpi|dpcm|dppx|%| |;)/gi;
-	const REGEXP_REPLACE_4_ZEROS = /:0 0 0 0(;|\})/g;
-	const REGEXP_REPLACE_3_ZEROS = /:0 0 0(;|\})/g;
-	// const REGEXP_REPLACE_2_ZEROS = /:0 0(;|\})/g;
-	const REGEXP_REPLACE_1_ZERO = /(transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin|box-shadow):0(;|\})/gi;
-	const REGEXP_REPLACE_ZERO_DOT_DECIMAL = /(:|\s)0+\.(\d+)/g;
-	const REGEXP_REPLACE_RGB = /rgb\s*\(\s*([0-9,\s]+)\s*\)/gi;
-	const REGEXP_REPLACE_BORDER_ZERO = /(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|\})/gi;
-	const REGEXP_REPLACE_IE_OPACITY = /progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi;
-	const REGEXP_REPLACE_QUERY_FRACTION = /\(([-A-Za-z]+):([0-9]+)\/([0-9]+)\)/g;
-	const REGEXP_QUERY_FRACTION = /___QUERY_FRACTION___/g;
-	const REGEXP_REPLACE_SEMI_COLUMNS = /;;+/g;
-	const REGEXP_REPLACE_HASH_COLOR = /(:|\s)(#f00)(;|})/g;
-	const REGEXP_PRESERVED_NEWLINE = /___PRESERVED_NEWLINE___/g;
-	const REGEXP_REPLACE_HASH_COLOR_SHORT1 = /(:|\s)(#000080)(;|})/g;
-	const REGEXP_REPLACE_HASH_COLOR_SHORT2 = /(:|\s)(#808080)(;|})/g;
-	const REGEXP_REPLACE_HASH_COLOR_SHORT3 = /(:|\s)(#808000)(;|})/g;
-	const REGEXP_REPLACE_HASH_COLOR_SHORT4 = /(:|\s)(#800080)(;|})/g;
-	const REGEXP_REPLACE_HASH_COLOR_SHORT5 = /(:|\s)(#c0c0c0)(;|})/g;
-	const REGEXP_REPLACE_HASH_COLOR_SHORT6 = /(:|\s)(#008080)(;|})/g;
-	const REGEXP_REPLACE_HASH_COLOR_SHORT7 = /(:|\s)(#ffa500)(;|})/g;
-	const REGEXP_REPLACE_HASH_COLOR_SHORT8 = /(:|\s)(#800000)(;|})/g;
-
-	function processString(content = "", options = defaultOptions) {
-
-		const comments = [];
-		const preservedTokens = [];
-
-		let pattern;
-
-		const originalContent = content;
-		content = extractDataUrls(content, preservedTokens);
-		content = collectComments(content, comments);
-
-		// preserve strings so their content doesn't get accidentally minified
-		pattern = REGEXP_PRESERVE_STRING;
-		content = content.replace(pattern, token => {
-			const quote = token.substring(0, 1);
-			token = token.slice(1, -1);
-			// maybe the string contains a comment-like substring or more? put'em back then
-			if (token.indexOf("___PRESERVE_CANDIDATE_COMMENT_") >= 0) {
-				for (let i = 0, len = comments.length; i < len; i += 1) {
-					token = token.replace("___PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]);
-				}
-			}
-			// minify alpha opacity in filter strings
-			token = token.replace(REGEXP_MINIFY_ALPHA, "alpha(opacity=");
-			preservedTokens.push(token);
-			return quote + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___" + quote;
-		});
+	table.push(content.slice(end + 2));
 
-		// strings are safe, now wrestle the comments
-		for (let i = 0, len = comments.length; i < len; i += 1) {
-
-			const token = comments[i];
-			const placeholder = "___PRESERVE_CANDIDATE_COMMENT_" + i + "___";
-
-			// ! in the first position of the comment means preserve
-			// so push to the preserved tokens keeping the !
-			if (token.charAt(0) === "!") {
-				if (options.cuteComments) {
-					preservedTokens.push(token.substring(1).replace(REGEXP_PRESERVE_TOKEN1, "\n"));
-				} else if (options.uglyComments) {
-					preservedTokens.push(token.substring(1).replace(REGEXP_PRESERVE_TOKEN2, ""));
-				} else {
-					preservedTokens.push(token);
-				}
-				content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-				continue;
-			}
+	return table.join("");
+}
 
-			// \ in the last position looks like hack for Mac/IE5
-			// shorten that to /*\*/ and the next one to /**/
-			if (token.charAt(token.length - 1) === "\\") {
-				preservedTokens.push("\\");
-				content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-				i = i + 1; // attn: advancing the loop
-				preservedTokens.push("");
-				content = content.replace(
-					"___PRESERVE_CANDIDATE_COMMENT_" + i + "___",
-					___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___"
-				);
-				continue;
-			}
+/**
+ * processString uglifies a CSS string
+ *
+ * @param {string} content - CSS string
+ * @param {options} options - UglifyCSS options
+ *
+ * @return {string} Uglified result
+ */
 
-			// keep empty comments after child selectors (IE7 hack)
-			// e.g. html >/**/ body
-			if (token.length === 0) {
-				const startIndex = content.indexOf(placeholder);
-				if (startIndex > 2) {
-					if (content.charAt(startIndex - 3) === ">") {
-						preservedTokens.push("");
-						content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-					}
-				}
+// const REGEXP_EMPTY_RULES = /[^};{/]+\{\}/g;
+const REGEXP_PRESERVE_STRING = /("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g;
+const REGEXP_MINIFY_ALPHA = /progid:DXImageTransform.Microsoft.Alpha\(Opacity=/gi;
+const REGEXP_PRESERVE_TOKEN1 = /\r\n/g;
+const REGEXP_PRESERVE_TOKEN2 = /[\r\n]/g;
+const REGEXP_VARIABLES = /@variables\s*\{\s*([^}]+)\s*\}/g;
+const REGEXP_VARIABLE = /\s*([a-z0-9-]+)\s*:\s*([^;}]+)\s*/gi;
+const REGEXP_VARIABLE_VALUE = /var\s*\(\s*([^)]+)\s*\)/g;
+const REGEXP_PRESERVE_CALC = /calc\(([^;}]*)\)/g;
+const REGEXP_TRIM = /(^\s*|\s*$)/g;
+const REGEXP_PRESERVE_CALC2 = /\( /g;
+const REGEXP_PRESERVE_CALC3 = / \)/g;
+const REGEXP_PRESERVE_MATRIX = /\s*filter:\s*progid:DXImageTransform.Microsoft.Matrix\(([^)]+)\);/g;
+const REGEXP_REMOVE_SPACES = /(^|\})(([^{:])+:)+([^{]*{)/g;
+const REGEXP_REMOVE_SPACES2 = /\s+([!{;:>+()\],])/g;
+const REGEXP_REMOVE_SPACES2_BIS = /([^\\])\s+([}])/g;
+const REGEXP_RESTORE_SPACE_IMPORTANT = /!important/g;
+const REGEXP_PSEUDOCLASSCOLON = /___PSEUDOCLASSCOLON___/g;
+const REGEXP_COLUMN = /:/g;
+const REGEXP_PRESERVE_ZERO_UNIT = /\s*(animation|animation-delay|animation-duration|transition|transition-delay|transition-duration):\s*([^;}]+)/gi;
+const REGEXP_PRESERVE_ZERO_UNIT1 = /(^|\D)0?\.?0(m?s)/gi;
+const REGEXP_PRESERVE_FLEX = /\s*(flex|flex-basis):\s*([^;}]+)/gi;
+const REGEXP_SPACES = /\s+/;
+const REGEXP_PRESERVE_HSLA = /(hsla?)\(([^)]+)\)/g;
+const REGEXP_PRESERVE_HSLA1 = /(^\s+|\s+$)/g;
+const REGEXP_RETAIN_SPACE_IE6 = /:first-(line|letter)(\{|,)/gi;
+const REGEXP_CHARSET = /^(.*)(@charset)( "[^"]*";)/gi;
+const REGEXP_REMOVE_SECOND_CHARSET = /^((\s*)(@charset)( [^;]+;\s*))+/gi;
+const REGEXP_LOWERCASE_DIRECTIVES = /@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/gi;
+const REGEXP_LOWERCASE_PSEUDO_ELEMENTS = /:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/gi;
+const REGEXP_CHARSET2 = /^(.*)(@charset "[^"]*";)/g;
+const REGEXP_CHARSET3 = /^(\s*@charset [^;]+;\s*)+/g;
+const REGEXP_LOWERCASE_FUNCTIONS = /:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?any)\(/gi;
+const REGEXP_LOWERCASE_FUNCTIONS2 = /([:,( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/gi;
+const REGEXP_NEWLINE1 = /\s*\/\*/g;
+const REGEXP_NEWLINE2 = /\*\/\s*/g;
+const REGEXP_RESTORE_SPACE1 = /\band\(/gi;
+const REGEXP_RESTORE_SPACE2 = /([^:])not\(/gi;
+const REGEXP_RESTORE_SPACE3 = /\bor\(/gi;
+const REGEXP_REMOVE_SPACES3 = /([!{}:;>+([,])\s+/g;
+const REGEXP_REMOVE_SEMI_COLUMNS = /;+\}/g;
+// const REGEXP_REPLACE_ZERO = /(^|[^.0-9\\])(?:0?\.)?0(?:ex|ch|r?em|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|g?rad|turn|ms|k?Hz|dpi|dpcm|dppx|%)(?![a-z0-9])/gi;
+const REGEXP_REPLACE_ZERO_DOT = /([0-9])\.0(ex|ch|r?em|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|g?rad|turn|m?s|k?Hz|dpi|dpcm|dppx|%| |;)/gi;
+const REGEXP_REPLACE_4_ZEROS = /:0 0 0 0(;|\})/g;
+const REGEXP_REPLACE_3_ZEROS = /:0 0 0(;|\})/g;
+// const REGEXP_REPLACE_2_ZEROS = /:0 0(;|\})/g;
+const REGEXP_REPLACE_1_ZERO = /(transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin|box-shadow):0(;|\})/gi;
+const REGEXP_REPLACE_ZERO_DOT_DECIMAL = /(:|\s)0+\.(\d+)/g;
+const REGEXP_REPLACE_RGB = /rgb\s*\(\s*([0-9,\s]+)\s*\)/gi;
+const REGEXP_REPLACE_BORDER_ZERO = /(border|border-top|border-right|border-bottom|border-left|outline|background):none(;|\})/gi;
+const REGEXP_REPLACE_IE_OPACITY = /progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi;
+const REGEXP_REPLACE_QUERY_FRACTION = /\(([-A-Za-z]+):([0-9]+)\/([0-9]+)\)/g;
+const REGEXP_QUERY_FRACTION = /___QUERY_FRACTION___/g;
+const REGEXP_REPLACE_SEMI_COLUMNS = /;;+/g;
+const REGEXP_REPLACE_HASH_COLOR = /(:|\s)(#f00)(;|})/g;
+const REGEXP_PRESERVED_NEWLINE = /___PRESERVED_NEWLINE___/g;
+const REGEXP_REPLACE_HASH_COLOR_SHORT1 = /(:|\s)(#000080)(;|})/g;
+const REGEXP_REPLACE_HASH_COLOR_SHORT2 = /(:|\s)(#808080)(;|})/g;
+const REGEXP_REPLACE_HASH_COLOR_SHORT3 = /(:|\s)(#808000)(;|})/g;
+const REGEXP_REPLACE_HASH_COLOR_SHORT4 = /(:|\s)(#800080)(;|})/g;
+const REGEXP_REPLACE_HASH_COLOR_SHORT5 = /(:|\s)(#c0c0c0)(;|})/g;
+const REGEXP_REPLACE_HASH_COLOR_SHORT6 = /(:|\s)(#008080)(;|})/g;
+const REGEXP_REPLACE_HASH_COLOR_SHORT7 = /(:|\s)(#ffa500)(;|})/g;
+const REGEXP_REPLACE_HASH_COLOR_SHORT8 = /(:|\s)(#800000)(;|})/g;
+
+function processString(content = "", options = defaultOptions) {
+
+	const comments = [];
+	const preservedTokens = [];
+
+	let pattern;
+
+	const originalContent = content;
+	content = extractDataUrls(content, preservedTokens);
+	content = collectComments(content, comments);
+
+	// preserve strings so their content doesn't get accidentally minified
+	pattern = REGEXP_PRESERVE_STRING;
+	content = content.replace(pattern, token => {
+		const quote = token.substring(0, 1);
+		token = token.slice(1, -1);
+		// maybe the string contains a comment-like substring or more? put'em back then
+		if (token.indexOf("___PRESERVE_CANDIDATE_COMMENT_") >= 0) {
+			for (let i = 0, len = comments.length; i < len; i += 1) {
+				token = token.replace("___PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]);
 			}
-
-			// in all other cases kill the comment
-			content = content.replace(`/*${placeholder}*/`, "");
 		}
-
-		// parse simple @variables blocks and remove them
-		if (options.expandVars) {
-			const vars = {};
-			pattern = REGEXP_VARIABLES;
-			content = content.replace(pattern, (_, f1) => {
-				pattern = REGEXP_VARIABLE;
-				f1.replace(pattern, (_, f1, f2) => {
-					if (f1 && f2) {
-						vars[f1] = f2;
-					}
-					return "";
-				});
-				return "";
-			});
-
-			// replace var(x) with the value of x
-			pattern = REGEXP_VARIABLE_VALUE;
-			content = content.replace(pattern, (_, f1) => {
-				return vars[f1] || "none";
-			});
+		// minify alpha opacity in filter strings
+		token = token.replace(REGEXP_MINIFY_ALPHA, "alpha(opacity=");
+		preservedTokens.push(token);
+		return quote + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___" + quote;
+	});
+
+	// strings are safe, now wrestle the comments
+	for (let i = 0, len = comments.length; i < len; i += 1) {
+
+		const token = comments[i];
+		const placeholder = "___PRESERVE_CANDIDATE_COMMENT_" + i + "___";
+
+		// ! in the first position of the comment means preserve
+		// so push to the preserved tokens keeping the !
+		if (token.charAt(0) === "!") {
+			if (options.cuteComments) {
+				preservedTokens.push(token.substring(1).replace(REGEXP_PRESERVE_TOKEN1, "\n"));
+			} else if (options.uglyComments) {
+				preservedTokens.push(token.substring(1).replace(REGEXP_PRESERVE_TOKEN2, ""));
+			} else {
+				preservedTokens.push(token);
+			}
+			content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+			continue;
 		}
 
-		// normalize all whitespace strings to single spaces. Easier to work with that way.
-		content = content.replace(REGEXP_WHITE_SPACES, " ");
-
-		// preserve formulas in calc() before removing spaces
-		pattern = REGEXP_PRESERVE_CALC;
-		content = content.replace(pattern, (_, f1) => {
-			preservedTokens.push(
-				"calc(" +
-				f1.replace(REGEXP_TRIM, "")
-					.replace(REGEXP_PRESERVE_CALC2, "(")
-					.replace(REGEXP_PRESERVE_CALC3, ")") +
-				")"
+		// \ in the last position looks like hack for Mac/IE5
+		// shorten that to /*\*/ and the next one to /**/
+		if (token.charAt(token.length - 1) === "\\") {
+			preservedTokens.push("\\");
+			content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+			i = i + 1; // attn: advancing the loop
+			preservedTokens.push("");
+			content = content.replace(
+				"___PRESERVE_CANDIDATE_COMMENT_" + i + "___",
+				___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___"
 			);
-			return ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
-		});
-
-		// preserve matrix
-		pattern = REGEXP_PRESERVE_MATRIX;
-		content = content.replace(pattern, (_, f1) => {
-			preservedTokens.push(f1);
-			return "filter:progid:DXImageTransform.Microsoft.Matrix(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___);";
-		});
-
-		// remove the spaces before the things that should not have spaces before them.
-		// but, be careful not to turn 'p :link {...}' into 'p:link{...}'
-		// swap out any pseudo-class colons with the token, and then swap back.
-		pattern = REGEXP_REMOVE_SPACES;
-		content = content.replace(pattern, token => token.replace(REGEXP_COLUMN, "___PSEUDOCLASSCOLON___"));
-
-		// remove spaces before the things that should not have spaces before them.
-		content = content.replace(REGEXP_REMOVE_SPACES2, "$1");
-		content = content.replace(REGEXP_REMOVE_SPACES2_BIS, "$1$2");
-
-		// restore spaces for !important
-		content = content.replace(REGEXP_RESTORE_SPACE_IMPORTANT, " !important");
+			continue;
+		}
 
-		// bring back the colon
-		content = content.replace(REGEXP_PSEUDOCLASSCOLON, ":");
+		// keep empty comments after child selectors (IE7 hack)
+		// e.g. html >/**/ body
+		if (token.length === 0) {
+			const startIndex = content.indexOf(placeholder);
+			if (startIndex > 2) {
+				if (content.charAt(startIndex - 3) === ">") {
+					preservedTokens.push("");
+					content = content.replace(placeholder, ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+				}
+			}
+		}
 
-		// preserve 0 followed by a time unit for properties using time units
-		pattern = REGEXP_PRESERVE_ZERO_UNIT;
-		content = content.replace(pattern, (_, f1, f2) => {
+		// in all other cases kill the comment
+		content = content.replace(`/*${placeholder}*/`, "");
+	}
 
-			f2 = f2.replace(REGEXP_PRESERVE_ZERO_UNIT1, (_, g1, g2) => {
-				preservedTokens.push("0" + g2);
-				return g1 + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
+	// parse simple @variables blocks and remove them
+	if (options.expandVars) {
+		const vars = {};
+		pattern = REGEXP_VARIABLES;
+		content = content.replace(pattern, (_, f1) => {
+			pattern = REGEXP_VARIABLE;
+			f1.replace(pattern, (_, f1, f2) => {
+				if (f1 && f2) {
+					vars[f1] = f2;
+				}
+				return "";
 			});
-
-			return f1 + ":" + f2;
+			return "";
 		});
 
-		// preserve unit for flex-basis within flex and flex-basis (ie10 bug)
-		pattern = REGEXP_PRESERVE_FLEX;
-		content = content.replace(pattern, (_, f1, f2) => {
-			let f2b = f2.split(REGEXP_SPACES);
-			preservedTokens.push(f2b.pop());
-			f2b.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-			f2b = f2b.join(" ");
-			return `${f1}:${f2b}`;
+		// replace var(x) with the value of x
+		pattern = REGEXP_VARIABLE_VALUE;
+		content = content.replace(pattern, (_, f1) => {
+			return vars[f1] || "none";
 		});
+	}
 
-		// preserve 0% in hsl and hsla color definitions
-		content = content.replace(REGEXP_PRESERVE_HSLA, (_, f1, f2) => {
-			const f0 = [];
-			f2.split(",").forEach(part => {
-				part = part.replace(REGEXP_PRESERVE_HSLA1, "");
-				if (part === "0%") {
-					preservedTokens.push("0%");
-					f0.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
-				} else {
-					f0.push(part);
-				}
-			});
-			return f1 + "(" + f0.join(",") + ")";
+	// normalize all whitespace strings to single spaces. Easier to work with that way.
+	content = content.replace(REGEXP_WHITE_SPACES, " ");
+
+	// preserve formulas in calc() before removing spaces
+	pattern = REGEXP_PRESERVE_CALC;
+	content = content.replace(pattern, (_, f1) => {
+		preservedTokens.push(
+			"calc(" +
+			f1.replace(REGEXP_TRIM, "")
+				.replace(REGEXP_PRESERVE_CALC2, "(")
+				.replace(REGEXP_PRESERVE_CALC3, ")") +
+			")"
+		);
+		return ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
+	});
+
+	// preserve matrix
+	pattern = REGEXP_PRESERVE_MATRIX;
+	content = content.replace(pattern, (_, f1) => {
+		preservedTokens.push(f1);
+		return "filter:progid:DXImageTransform.Microsoft.Matrix(" + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___);";
+	});
+
+	// remove the spaces before the things that should not have spaces before them.
+	// but, be careful not to turn 'p :link {...}' into 'p:link{...}'
+	// swap out any pseudo-class colons with the token, and then swap back.
+	pattern = REGEXP_REMOVE_SPACES;
+	content = content.replace(pattern, token => token.replace(REGEXP_COLUMN, "___PSEUDOCLASSCOLON___"));
+
+	// remove spaces before the things that should not have spaces before them.
+	content = content.replace(REGEXP_REMOVE_SPACES2, "$1");
+	content = content.replace(REGEXP_REMOVE_SPACES2_BIS, "$1$2");
+
+	// restore spaces for !important
+	content = content.replace(REGEXP_RESTORE_SPACE_IMPORTANT, " !important");
+
+	// bring back the colon
+	content = content.replace(REGEXP_PSEUDOCLASSCOLON, ":");
+
+	// preserve 0 followed by a time unit for properties using time units
+	pattern = REGEXP_PRESERVE_ZERO_UNIT;
+	content = content.replace(pattern, (_, f1, f2) => {
+
+		f2 = f2.replace(REGEXP_PRESERVE_ZERO_UNIT1, (_, g1, g2) => {
+			preservedTokens.push("0" + g2);
+			return g1 + ___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___";
 		});
 
-		// preserve 0 followed by unit in keyframes steps (WIP)
-		content = keyframes(content, preservedTokens);
-
-		// retain space for special IE6 cases
-		content = content.replace(REGEXP_RETAIN_SPACE_IE6, (_, f1, f2) => ":first-" + f1.toLowerCase() + " " + f2);
-
-		// newlines before and after the end of a preserved comment
-		if (options.cuteComments) {
-			content = content.replace(REGEXP_NEWLINE1, "___PRESERVED_NEWLINE___/*");
-			content = content.replace(REGEXP_NEWLINE2, "*/___PRESERVED_NEWLINE___");
-			// no space after the end of a preserved comment
-		} else {
-			content = content.replace(REGEXP_NEWLINE2, "*/");
-		}
-
-		// If there are multiple @charset directives, push them to the top of the file.
-		pattern = REGEXP_CHARSET;
-		content = content.replace(pattern, (_, f1, f2, f3) => f2.toLowerCase() + f3 + f1);
-
-		// When all @charset are at the top, remove the second and after (as they are completely ignored).
-		pattern = REGEXP_REMOVE_SECOND_CHARSET;
-		content = content.replace(pattern, (_, __, f2, f3, f4) => f2 + f3.toLowerCase() + f4);
-
-		// lowercase some popular @directives (@charset is done right above)
-		pattern = REGEXP_LOWERCASE_DIRECTIVES;
-		content = content.replace(pattern, (_, f1) => "@" + f1.toLowerCase());
-
-		// lowercase some more common pseudo-elements
-		pattern = REGEXP_LOWERCASE_PSEUDO_ELEMENTS;
-		content = content.replace(pattern, (_, f1) => ":" + f1.toLowerCase());
-
-		// if there is a @charset, then only allow one, and push to the top of the file.
-		content = content.replace(REGEXP_CHARSET2, "$2$1");
-		content = content.replace(REGEXP_CHARSET3, "$1");
-
-		// lowercase some more common functions
-		pattern = REGEXP_LOWERCASE_FUNCTIONS;
-		content = content.replace(pattern, (_, f1) => ":" + f1.toLowerCase() + "(");
-
-		// lower case some common function that can be values
-		// NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
-		pattern = REGEXP_LOWERCASE_FUNCTIONS2;
-		content = content.replace(pattern, (_, f1, f2) => f1 + f2.toLowerCase());
-
-		// put the space back in some cases, to support stuff like
-		// @media screen and (-webkit-min-device-pixel-ratio:0){
-		content = content.replace(REGEXP_RESTORE_SPACE1, "and (");
-		content = content.replace(REGEXP_RESTORE_SPACE2, "$1not (");
-		content = content.replace(REGEXP_RESTORE_SPACE3, "or (");
-
-		// remove the spaces after the things that should not have spaces after them.
-		content = content.replace(REGEXP_REMOVE_SPACES3, "$1");
-
-		// remove unnecessary semicolons
-		content = content.replace(REGEXP_REMOVE_SEMI_COLUMNS, "}");
-
-		// replace 0(px,em,%) with 0.
-		// content = content.replace(REGEXP_REPLACE_ZERO, "$10");
-
-		// Replace x.0(px,em,%) with x(px,em,%).
-		content = content.replace(REGEXP_REPLACE_ZERO_DOT, "$1$2");
-
-		// replace 0 0 0 0; with 0.
-		content = content.replace(REGEXP_REPLACE_4_ZEROS, ":0$1");
-		content = content.replace(REGEXP_REPLACE_3_ZEROS, ":0$1");
-		// content = content.replace(REGEXP_REPLACE_2_ZEROS, ":0$1");
-
-		// replace background-position:0; with background-position:0 0;
-		// same for transform-origin and box-shadow
-		pattern = REGEXP_REPLACE_1_ZERO;
-		content = content.replace(pattern, (_, f1, f2) => f1.toLowerCase() + ":0 0" + f2);
-
-		// replace 0.6 to .6, but only when preceded by : or a white-space
-		content = content.replace(REGEXP_REPLACE_ZERO_DOT_DECIMAL, "$1.$2");
-
-		// shorten colors from rgb(51,102,153) to #336699
-		// this makes it more likely that it'll get further compressed in the next step.
-		pattern = REGEXP_REPLACE_RGB;
-		content = content.replace(pattern, (_, f1) => {
-			const rgbcolors = f1.split(",");
-			let hexcolor = "#";
-			for (let i = 0; i < rgbcolors.length; i += 1) {
-				let val = parseInt(rgbcolors[i], 10);
-				if (val < 16) {
-					hexcolor += "0";
-				}
-				if (val > 255) {
-					val = 255;
-				}
-				hexcolor += val.toString(16);
+		return f1 + ":" + f2;
+	});
+
+	// preserve unit for flex-basis within flex and flex-basis (ie10 bug)
+	pattern = REGEXP_PRESERVE_FLEX;
+	content = content.replace(pattern, (_, f1, f2) => {
+		let f2b = f2.split(REGEXP_SPACES);
+		preservedTokens.push(f2b.pop());
+		f2b.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+		f2b = f2b.join(" ");
+		return `${f1}:${f2b}`;
+	});
+
+	// preserve 0% in hsl and hsla color definitions
+	content = content.replace(REGEXP_PRESERVE_HSLA, (_, f1, f2) => {
+		const f0 = [];
+		f2.split(",").forEach(part => {
+			part = part.replace(REGEXP_PRESERVE_HSLA1, "");
+			if (part === "0%") {
+				preservedTokens.push("0%");
+				f0.push(___PRESERVED_TOKEN_ + (preservedTokens.length - 1) + "___");
+			} else {
+				f0.push(part);
 			}
-			return hexcolor;
 		});
+		return f1 + "(" + f0.join(",") + ")";
+	});
+
+	// preserve 0 followed by unit in keyframes steps (WIP)
+	content = keyframes(content, preservedTokens);
+
+	// retain space for special IE6 cases
+	content = content.replace(REGEXP_RETAIN_SPACE_IE6, (_, f1, f2) => ":first-" + f1.toLowerCase() + " " + f2);
+
+	// newlines before and after the end of a preserved comment
+	if (options.cuteComments) {
+		content = content.replace(REGEXP_NEWLINE1, "___PRESERVED_NEWLINE___/*");
+		content = content.replace(REGEXP_NEWLINE2, "*/___PRESERVED_NEWLINE___");
+		// no space after the end of a preserved comment
+	} else {
+		content = content.replace(REGEXP_NEWLINE2, "*/");
+	}
 
-		// Shorten colors from #AABBCC to #ABC.
-		content = compressHexColors(content);
-
-		// Replace #f00 -> red
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR, "$1red$3");
-
-		// Replace other short color keywords
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT1, "$1navy$3");
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT2, "$1gray$3");
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT3, "$1olive$3");
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT4, "$1purple$3");
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT5, "$1silver$3");
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT6, "$1teal$3");
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT7, "$1orange$3");
-		content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT8, "$1maroon$3");
-
-		// border: none -> border:0
-		pattern = REGEXP_REPLACE_BORDER_ZERO;
-		content = content.replace(pattern, (_, f1, f2) => f1.toLowerCase() + ":0" + f2);
-
-		// shorter opacity IE filter
-		content = content.replace(REGEXP_REPLACE_IE_OPACITY, "alpha(opacity=");
-
-		// Find a fraction that is used for Opera's -o-device-pixel-ratio query
-		// Add token to add the '\' back in later
-		content = content.replace(REGEXP_REPLACE_QUERY_FRACTION, "($1:$2___QUERY_FRACTION___$3)");
-
-		// remove empty rules.
-		// content = content.replace(REGEXP_EMPTY_RULES, "");
-
-		// Add '\' back to fix Opera -o-device-pixel-ratio query
-		content = content.replace(REGEXP_QUERY_FRACTION, "/");
-
-		// some source control tools don't like it when files containing lines longer
-		// than, say 8000 characters, are checked in. The linebreak option is used in
-		// that case to split long lines after a specific column.
-		if (options.maxLineLen > 0) {
-			const lines = [];
-			let line = [];
-			for (let i = 0, len = content.length; i < len; i += 1) {
-				const ch = content.charAt(i);
-				line.push(ch);
-				if (ch === "}" && line.length > options.maxLineLen) {
-					lines.push(line.join(""));
-					line = [];
-				}
+	// If there are multiple @charset directives, push them to the top of the file.
+	pattern = REGEXP_CHARSET;
+	content = content.replace(pattern, (_, f1, f2, f3) => f2.toLowerCase() + f3 + f1);
+
+	// When all @charset are at the top, remove the second and after (as they are completely ignored).
+	pattern = REGEXP_REMOVE_SECOND_CHARSET;
+	content = content.replace(pattern, (_, __, f2, f3, f4) => f2 + f3.toLowerCase() + f4);
+
+	// lowercase some popular @directives (@charset is done right above)
+	pattern = REGEXP_LOWERCASE_DIRECTIVES;
+	content = content.replace(pattern, (_, f1) => "@" + f1.toLowerCase());
+
+	// lowercase some more common pseudo-elements
+	pattern = REGEXP_LOWERCASE_PSEUDO_ELEMENTS;
+	content = content.replace(pattern, (_, f1) => ":" + f1.toLowerCase());
+
+	// if there is a @charset, then only allow one, and push to the top of the file.
+	content = content.replace(REGEXP_CHARSET2, "$2$1");
+	content = content.replace(REGEXP_CHARSET3, "$1");
+
+	// lowercase some more common functions
+	pattern = REGEXP_LOWERCASE_FUNCTIONS;
+	content = content.replace(pattern, (_, f1) => ":" + f1.toLowerCase() + "(");
+
+	// lower case some common function that can be values
+	// NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us right after this
+	pattern = REGEXP_LOWERCASE_FUNCTIONS2;
+	content = content.replace(pattern, (_, f1, f2) => f1 + f2.toLowerCase());
+
+	// put the space back in some cases, to support stuff like
+	// @media screen and (-webkit-min-device-pixel-ratio:0){
+	content = content.replace(REGEXP_RESTORE_SPACE1, "and (");
+	content = content.replace(REGEXP_RESTORE_SPACE2, "$1not (");
+	content = content.replace(REGEXP_RESTORE_SPACE3, "or (");
+
+	// remove the spaces after the things that should not have spaces after them.
+	content = content.replace(REGEXP_REMOVE_SPACES3, "$1");
+
+	// remove unnecessary semicolons
+	content = content.replace(REGEXP_REMOVE_SEMI_COLUMNS, "}");
+
+	// replace 0(px,em,%) with 0.
+	// content = content.replace(REGEXP_REPLACE_ZERO, "$10");
+
+	// Replace x.0(px,em,%) with x(px,em,%).
+	content = content.replace(REGEXP_REPLACE_ZERO_DOT, "$1$2");
+
+	// replace 0 0 0 0; with 0.
+	content = content.replace(REGEXP_REPLACE_4_ZEROS, ":0$1");
+	content = content.replace(REGEXP_REPLACE_3_ZEROS, ":0$1");
+	// content = content.replace(REGEXP_REPLACE_2_ZEROS, ":0$1");
+
+	// replace background-position:0; with background-position:0 0;
+	// same for transform-origin and box-shadow
+	pattern = REGEXP_REPLACE_1_ZERO;
+	content = content.replace(pattern, (_, f1, f2) => f1.toLowerCase() + ":0 0" + f2);
+
+	// replace 0.6 to .6, but only when preceded by : or a white-space
+	content = content.replace(REGEXP_REPLACE_ZERO_DOT_DECIMAL, "$1.$2");
+
+	// shorten colors from rgb(51,102,153) to #336699
+	// this makes it more likely that it'll get further compressed in the next step.
+	pattern = REGEXP_REPLACE_RGB;
+	content = content.replace(pattern, (_, f1) => {
+		const rgbcolors = f1.split(",");
+		let hexcolor = "#";
+		for (let i = 0; i < rgbcolors.length; i += 1) {
+			let val = parseInt(rgbcolors[i], 10);
+			if (val < 16) {
+				hexcolor += "0";
+			}
+			if (val > 255) {
+				val = 255;
 			}
-			if (line.length) {
+			hexcolor += val.toString(16);
+		}
+		return hexcolor;
+	});
+
+	// Shorten colors from #AABBCC to #ABC.
+	content = compressHexColors(content);
+
+	// Replace #f00 -> red
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR, "$1red$3");
+
+	// Replace other short color keywords
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT1, "$1navy$3");
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT2, "$1gray$3");
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT3, "$1olive$3");
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT4, "$1purple$3");
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT5, "$1silver$3");
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT6, "$1teal$3");
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT7, "$1orange$3");
+	content = content.replace(REGEXP_REPLACE_HASH_COLOR_SHORT8, "$1maroon$3");
+
+	// border: none -> border:0
+	pattern = REGEXP_REPLACE_BORDER_ZERO;
+	content = content.replace(pattern, (_, f1, f2) => f1.toLowerCase() + ":0" + f2);
+
+	// shorter opacity IE filter
+	content = content.replace(REGEXP_REPLACE_IE_OPACITY, "alpha(opacity=");
+
+	// Find a fraction that is used for Opera's -o-device-pixel-ratio query
+	// Add token to add the '\' back in later
+	content = content.replace(REGEXP_REPLACE_QUERY_FRACTION, "($1:$2___QUERY_FRACTION___$3)");
+
+	// remove empty rules.
+	// content = content.replace(REGEXP_EMPTY_RULES, "");
+
+	// Add '\' back to fix Opera -o-device-pixel-ratio query
+	content = content.replace(REGEXP_QUERY_FRACTION, "/");
+
+	// some source control tools don't like it when files containing lines longer
+	// than, say 8000 characters, are checked in. The linebreak option is used in
+	// that case to split long lines after a specific column.
+	if (options.maxLineLen > 0) {
+		const lines = [];
+		let line = [];
+		for (let i = 0, len = content.length; i < len; i += 1) {
+			const ch = content.charAt(i);
+			line.push(ch);
+			if (ch === "}" && line.length > options.maxLineLen) {
 				lines.push(line.join(""));
+				line = [];
 			}
-
-			content = lines.join("\n");
+		}
+		if (line.length) {
+			lines.push(line.join(""));
 		}
 
-		// replace multiple semi-colons in a row by a single one
-		// see SF bug #1980989
-		content = content.replace(REGEXP_REPLACE_SEMI_COLUMNS, ";");
-
-		// trim the final string (for any leading or trailing white spaces)
-		content = content.replace(REGEXP_TRIM, "");
+		content = lines.join("\n");
+	}
 
-		if (preservedTokens.length > 1000) {
-			return originalContent;
-		}
+	// replace multiple semi-colons in a row by a single one
+	// see SF bug #1980989
+	content = content.replace(REGEXP_REPLACE_SEMI_COLUMNS, ";");
 
-		// restore preserved tokens
-		for (let i = preservedTokens.length - 1; i >= 0; i--) {
-			content = content.replace(___PRESERVED_TOKEN_ + i + "___", preservedTokens[i], "g");
-		}
+	// trim the final string (for any leading or trailing white spaces)
+	content = content.replace(REGEXP_TRIM, "");
 
-		// restore preserved newlines
-		content = content.replace(REGEXP_PRESERVED_NEWLINE, "\n");
+	if (preservedTokens.length > 1000) {
+		return originalContent;
+	}
 
-		// return
-		return content;
+	// restore preserved tokens
+	for (let i = preservedTokens.length - 1; i >= 0; i--) {
+		content = content.replace(___PRESERVED_TOKEN_ + i + "___", preservedTokens[i], "g");
 	}
 
-	return {
-		defaultOptions,
-		processString
-	};
+	// restore preserved newlines
+	content = content.replace(REGEXP_PRESERVED_NEWLINE, "\n");
+
+	// return
+	return content;
+}
 
-})();
+export {
+	defaultOptions,
+	processString
+};

File diff suppressed because it is too large
+ 15 - 1
lib/single-file/vendor/css-tree.js


+ 20 - 24
lib/single-file/vendor/css-unescape.js

@@ -47,30 +47,26 @@
  * THE SOFTWARE.
  */
 
-this.singlefile.lib.vendor.cssUnescape = this.singlefile.lib.vendor.cssUnescape || (() => {
+const whitespace = "[\\x20\\t\\r\\n\\f]";
+const unescapeRegExp = new RegExp("\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig");
 
-	const whitespace = "[\\x20\\t\\r\\n\\f]";
-	const unescapeRegExp = new RegExp("\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig");
+export {
+	process
+};
 
-	return {
-		process
-	};
+function process(str) {
+	return str.replace(unescapeRegExp, (_, escaped, escapedWhitespace) => {
+		const high = "0x" + escaped - 0x10000;
 
-	function process(str) {
-		return str.replace(unescapeRegExp, (_, escaped, escapedWhitespace) => {
-			const high = "0x" + escaped - 0x10000;
-
-			// NaN means non-codepoint
-			// Workaround erroneous numeric interpretation of +"0x"
-			// eslint-disable-next-line no-self-compare
-			return high !== high || escapedWhitespace
-				? escaped
-				: high < 0
-					? // BMP codepoint
-					String.fromCharCode(high + 0x10000)
-					: // Supplemental Plane codepoint (surrogate pair)
-					String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00);
-		});
-	}
-
-})();
+		// NaN means non-codepoint
+		// Workaround erroneous numeric interpretation of +"0x"
+		// eslint-disable-next-line no-self-compare
+		return high !== high || escapedWhitespace
+			? escaped
+			: high < 0
+				? // BMP codepoint
+				String.fromCharCode(high + 0x10000)
+				: // Supplemental Plane codepoint (surrogate pair)
+				String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00);
+	});
+}

+ 262 - 266
lib/single-file/vendor/html-srcset-parser.js

@@ -40,304 +40,300 @@
  * (except for comments in parens).
  */
 
-this.singlefile.lib.vendor.srcsetParser = this.singlefile.lib.vendor.srcsetParser || (() => {
-
-	return {
-		process
-	};
-
-	// 1. Let input be the value passed to this algorithm.
-	function process(input) {
-
-		// UTILITY FUNCTIONS
-
-		// Manual is faster than RegEx
-		// http://bjorn.tipling.com/state-and-regular-expressions-in-javascript
-		// http://jsperf.com/whitespace-character/5
-		function isSpace(c) {
-			return (c === "\u0020" || // space
-				c === "\u0009" || // horizontal tab
-				c === "\u000A" || // new line
-				c === "\u000C" || // form feed
-				c === "\u000D");  // carriage return
-		}
+export {
+	process
+};
+
+// 1. Let input be the value passed to this algorithm.
+function process(input) {
+
+	// UTILITY FUNCTIONS
+
+	// Manual is faster than RegEx
+	// http://bjorn.tipling.com/state-and-regular-expressions-in-javascript
+	// http://jsperf.com/whitespace-character/5
+	function isSpace(c) {
+		return (c === "\u0020" || // space
+			c === "\u0009" || // horizontal tab
+			c === "\u000A" || // new line
+			c === "\u000C" || // form feed
+			c === "\u000D");  // carriage return
+	}
 
-		function collectCharacters(regEx) {
-			let chars;
-			const match = regEx.exec(input.substring(pos));
-			if (match) {
-				chars = match[0];
-				pos += chars.length;
-				return chars;
-			}
+	function collectCharacters(regEx) {
+		let chars;
+		const match = regEx.exec(input.substring(pos));
+		if (match) {
+			chars = match[0];
+			pos += chars.length;
+			return chars;
 		}
+	}
 
-		const inputLength = input.length;
-
-		// (Don"t use \s, to avoid matching non-breaking space)
-		/* eslint-disable no-control-regex */
-		const regexLeadingSpaces = /^[ \t\n\r\u000c]+/;
-		const regexLeadingCommasOrSpaces = /^[, \t\n\r\u000c]+/;
-		const regexLeadingNotSpaces = /^[^ \t\n\r\u000c]+/;
-		const regexTrailingCommas = /[,]+$/;
-		const regexNonNegativeInteger = /^\d+$/;
-		/* eslint-enable no-control-regex */
-
-		// ( Positive or negative or unsigned integers or decimals, without or without exponents.
-		// Must include at least one digit.
-		// According to spec tests any decimal point must be followed by a digit.
-		// No leading plus sign is allowed.)
-		// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
-		const regexFloatingPoint = /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
-
-		let url, descriptors, currentDescriptor, state, c,
-			// 2. Let position be a pointer into input, initially pointing at the start
-			//    of the string.
-			pos = 0;
-		// 3. Let candidates be an initially empty source set.
-		const candidates = [];
-
-		// 4. Splitting loop: Collect a sequence of characters that are space
-		//    characters or U+002C COMMA characters. If any U+002C COMMA characters
-		//    were collected, that is a parse error.		
-		while (true) { // eslint-disable-line no-constant-condition
-			collectCharacters(regexLeadingCommasOrSpaces);
-
-			// 5. If position is past the end of input, return candidates and abort these steps.
-			if (pos >= inputLength) {
-				return candidates; // (we"re done, this is the sole return path)
-			}
+	const inputLength = input.length;
+
+	// (Don"t use \s, to avoid matching non-breaking space)
+	/* eslint-disable no-control-regex */
+	const regexLeadingSpaces = /^[ \t\n\r\u000c]+/;
+	const regexLeadingCommasOrSpaces = /^[, \t\n\r\u000c]+/;
+	const regexLeadingNotSpaces = /^[^ \t\n\r\u000c]+/;
+	const regexTrailingCommas = /[,]+$/;
+	const regexNonNegativeInteger = /^\d+$/;
+	/* eslint-enable no-control-regex */
+
+	// ( Positive or negative or unsigned integers or decimals, without or without exponents.
+	// Must include at least one digit.
+	// According to spec tests any decimal point must be followed by a digit.
+	// No leading plus sign is allowed.)
+	// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
+	const regexFloatingPoint = /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
+
+	let url, descriptors, currentDescriptor, state, c,
+		// 2. Let position be a pointer into input, initially pointing at the start
+		//    of the string.
+		pos = 0;
+	// 3. Let candidates be an initially empty source set.
+	const candidates = [];
+
+	// 4. Splitting loop: Collect a sequence of characters that are space
+	//    characters or U+002C COMMA characters. If any U+002C COMMA characters
+	//    were collected, that is a parse error.		
+	while (true) { // eslint-disable-line no-constant-condition
+		collectCharacters(regexLeadingCommasOrSpaces);
+
+		// 5. If position is past the end of input, return candidates and abort these steps.
+		if (pos >= inputLength) {
+			return candidates; // (we"re done, this is the sole return path)
+		}
 
-			// 6. Collect a sequence of characters that are not space characters,
-			//    and let that be url.
-			url = collectCharacters(regexLeadingNotSpaces);
+		// 6. Collect a sequence of characters that are not space characters,
+		//    and let that be url.
+		url = collectCharacters(regexLeadingNotSpaces);
 
-			// 7. Let descriptors be a new empty list.
-			descriptors = [];
+		// 7. Let descriptors be a new empty list.
+		descriptors = [];
 
-			// 8. If url ends with a U+002C COMMA character (,), follow these substeps:
-			//		(1). Remove all trailing U+002C COMMA characters from url. If this removed
-			//         more than one character, that is a parse error.
-			if (url.slice(-1) === ",") {
-				url = url.replace(regexTrailingCommas, "");
-				// (Jump ahead to step 9 to skip tokenization and just push the candidate).
-				parseDescriptors();
+		// 8. If url ends with a U+002C COMMA character (,), follow these substeps:
+		//		(1). Remove all trailing U+002C COMMA characters from url. If this removed
+		//         more than one character, that is a parse error.
+		if (url.slice(-1) === ",") {
+			url = url.replace(regexTrailingCommas, "");
+			// (Jump ahead to step 9 to skip tokenization and just push the candidate).
+			parseDescriptors();
 
-				//	Otherwise, follow these substeps:
-			} else {
-				tokenize();
-			} // (close else of step 8)
+			//	Otherwise, follow these substeps:
+		} else {
+			tokenize();
+		} // (close else of step 8)
 
-			// 16. Return to the step labeled splitting loop.
-		} // (Close of big while loop.)
+		// 16. Return to the step labeled splitting loop.
+	} // (Close of big while loop.)
 
-		/**
-		 * Tokenizes descriptor properties prior to parsing
-		 * Returns undefined.
-		 */
-		function tokenize() {
+	/**
+	 * Tokenizes descriptor properties prior to parsing
+	 * Returns undefined.
+	 */
+	function tokenize() {
 
-			// 8.1. Descriptor tokeniser: Skip whitespace
-			collectCharacters(regexLeadingSpaces);
+		// 8.1. Descriptor tokeniser: Skip whitespace
+		collectCharacters(regexLeadingSpaces);
 
-			// 8.2. Let current descriptor be the empty string.
-			currentDescriptor = "";
+		// 8.2. Let current descriptor be the empty string.
+		currentDescriptor = "";
 
-			// 8.3. Let state be in descriptor.
-			state = "in descriptor";
+		// 8.3. Let state be in descriptor.
+		state = "in descriptor";
 
-			while (true) { // eslint-disable-line no-constant-condition
+		while (true) { // eslint-disable-line no-constant-condition
 
-				// 8.4. Let c be the character at position.
-				c = input.charAt(pos);
+			// 8.4. Let c be the character at position.
+			c = input.charAt(pos);
 
-				//  Do the following depending on the value of state.
-				//  For the purpose of this step, "EOF" is a special character representing
-				//  that position is past the end of input.
+			//  Do the following depending on the value of state.
+			//  For the purpose of this step, "EOF" is a special character representing
+			//  that position is past the end of input.
 
-				// In descriptor
-				if (state === "in descriptor") {
-					// Do the following, depending on the value of c:
+			// In descriptor
+			if (state === "in descriptor") {
+				// Do the following, depending on the value of c:
 
-					// Space character
-					// If current descriptor is not empty, append current descriptor to
-					// descriptors and let current descriptor be the empty string.
-					// Set state to after descriptor.
-					if (isSpace(c)) {
-						if (currentDescriptor) {
-							descriptors.push(currentDescriptor);
-							currentDescriptor = "";
-							state = "after descriptor";
-						}
-
-						// U+002C COMMA (,)
-						// Advance position to the next character in input. If current descriptor
-						// is not empty, append current descriptor to descriptors. Jump to the step
-						// labeled descriptor parser.
-					} else if (c === ",") {
-						pos += 1;
-						if (currentDescriptor) {
-							descriptors.push(currentDescriptor);
-						}
-						parseDescriptors();
-						return;
-
-						// U+0028 LEFT PARENTHESIS (()
-						// Append c to current descriptor. Set state to in parens.
-					} else if (c === "\u0028") {
-						currentDescriptor = currentDescriptor + c;
-						state = "in parens";
-
-						// EOF
-						// If current descriptor is not empty, append current descriptor to
-						// descriptors. Jump to the step labeled descriptor parser.
-					} else if (c === "") {
-						if (currentDescriptor) {
-							descriptors.push(currentDescriptor);
-						}
-						parseDescriptors();
-						return;
-
-						// Anything else
-						// Append c to current descriptor.
-					} else {
-						currentDescriptor = currentDescriptor + c;
+				// Space character
+				// If current descriptor is not empty, append current descriptor to
+				// descriptors and let current descriptor be the empty string.
+				// Set state to after descriptor.
+				if (isSpace(c)) {
+					if (currentDescriptor) {
+						descriptors.push(currentDescriptor);
+						currentDescriptor = "";
+						state = "after descriptor";
 					}
-					// (end "in descriptor"
 
-					// In parens
-				} else if (state === "in parens") {
+					// U+002C COMMA (,)
+					// Advance position to the next character in input. If current descriptor
+					// is not empty, append current descriptor to descriptors. Jump to the step
+					// labeled descriptor parser.
+				} else if (c === ",") {
+					pos += 1;
+					if (currentDescriptor) {
+						descriptors.push(currentDescriptor);
+					}
+					parseDescriptors();
+					return;
 
-					// U+0029 RIGHT PARENTHESIS ())
-					// Append c to current descriptor. Set state to in descriptor.
-					if (c === ")") {
-						currentDescriptor = currentDescriptor + c;
-						state = "in descriptor";
+					// U+0028 LEFT PARENTHESIS (()
+					// Append c to current descriptor. Set state to in parens.
+				} else if (c === "\u0028") {
+					currentDescriptor = currentDescriptor + c;
+					state = "in parens";
 
-						// EOF
-						// Append current descriptor to descriptors. Jump to the step labeled
-						// descriptor parser.
-					} else if (c === "") {
+					// EOF
+					// If current descriptor is not empty, append current descriptor to
+					// descriptors. Jump to the step labeled descriptor parser.
+				} else if (c === "") {
+					if (currentDescriptor) {
 						descriptors.push(currentDescriptor);
-						parseDescriptors();
-						return;
-
-						// Anything else
-						// Append c to current descriptor.
-					} else {
-						currentDescriptor = currentDescriptor + c;
 					}
+					parseDescriptors();
+					return;
 
-					// After descriptor
-				} else if (state === "after descriptor") {
+					// Anything else
+					// Append c to current descriptor.
+				} else {
+					currentDescriptor = currentDescriptor + c;
+				}
+				// (end "in descriptor"
+
+				// In parens
+			} else if (state === "in parens") {
+
+				// U+0029 RIGHT PARENTHESIS ())
+				// Append c to current descriptor. Set state to in descriptor.
+				if (c === ")") {
+					currentDescriptor = currentDescriptor + c;
+					state = "in descriptor";
+
+					// EOF
+					// Append current descriptor to descriptors. Jump to the step labeled
+					// descriptor parser.
+				} else if (c === "") {
+					descriptors.push(currentDescriptor);
+					parseDescriptors();
+					return;
+
+					// Anything else
+					// Append c to current descriptor.
+				} else {
+					currentDescriptor = currentDescriptor + c;
+				}
 
-					// Do the following, depending on the value of c:
-					// Space character: Stay in this state.
-					if (isSpace(c)) {
+				// After descriptor
+			} else if (state === "after descriptor") {
 
-						// EOF: Jump to the step labeled descriptor parser.
-					} else if (c === "") {
-						parseDescriptors();
-						return;
+				// Do the following, depending on the value of c:
+				// Space character: Stay in this state.
+				if (isSpace(c)) {
 
-						// Anything else
-						// Set state to in descriptor. Set position to the previous character in input.
-					} else {
-						state = "in descriptor";
-						pos -= 1;
+					// EOF: Jump to the step labeled descriptor parser.
+				} else if (c === "") {
+					parseDescriptors();
+					return;
+
+					// Anything else
+					// Set state to in descriptor. Set position to the previous character in input.
+				} else {
+					state = "in descriptor";
+					pos -= 1;
 
-					}
 				}
+			}
 
-				// Advance position to the next character in input.
-				pos += 1;
+			// Advance position to the next character in input.
+			pos += 1;
 
-				// Repeat this step.
-			} // (close while true loop)
-		}
+			// Repeat this step.
+		} // (close while true loop)
+	}
 
-		/**
-		 * Adds descriptor properties to a candidate, pushes to the candidates array
-		 * @return undefined
-		 */
-		// Declared outside of the while loop so that it"s only created once.
-		function parseDescriptors() {
-
-			// 9. Descriptor parser: Let error be no.
-			let pError = false,
-
-				// 10. Let width be absent.
-				// 11. Let density be absent.
-				// 12. Let future-compat-h be absent. (We"re implementing it now as h)
-				w, d, h, i,
-				desc, lastChar, value, intVal, floatVal;
-			const candidate = {};
-
-			// 13. For each descriptor in descriptors, run the appropriate set of steps
-			// from the following list:
-			for (i = 0; i < descriptors.length; i++) {
-				desc = descriptors[i];
-
-				lastChar = desc[desc.length - 1];
-				value = desc.substring(0, desc.length - 1);
-				intVal = parseInt(value, 10);
-				floatVal = parseFloat(value);
+	/**
+	 * Adds descriptor properties to a candidate, pushes to the candidates array
+	 * @return undefined
+	 */
+	// Declared outside of the while loop so that it"s only created once.
+	function parseDescriptors() {
+
+		// 9. Descriptor parser: Let error be no.
+		let pError = false,
+
+			// 10. Let width be absent.
+			// 11. Let density be absent.
+			// 12. Let future-compat-h be absent. (We"re implementing it now as h)
+			w, d, h, i,
+			desc, lastChar, value, intVal, floatVal;
+		const candidate = {};
+
+		// 13. For each descriptor in descriptors, run the appropriate set of steps
+		// from the following list:
+		for (i = 0; i < descriptors.length; i++) {
+			desc = descriptors[i];
+
+			lastChar = desc[desc.length - 1];
+			value = desc.substring(0, desc.length - 1);
+			intVal = parseInt(value, 10);
+			floatVal = parseFloat(value);
+
+			// If the descriptor consists of a valid non-negative integer followed by
+			// a U+0077 LATIN SMALL LETTER W character
+			if (regexNonNegativeInteger.test(value) && (lastChar === "w")) {
+
+				// If width and density are not both absent, then let error be yes.
+				if (w || d) { pError = true; }
+
+				// Apply the rules for parsing non-negative integers to the descriptor.
+				// If the result is zero, let error be yes.
+				// Otherwise, let width be the result.
+				if (intVal === 0) { pError = true; } else { w = intVal; }
+
+				// If the descriptor consists of a valid floating-point number followed by
+				// a U+0078 LATIN SMALL LETTER X character
+			} else if (regexFloatingPoint.test(value) && (lastChar === "x")) {
+
+				// If width, density and future-compat-h are not all absent, then let error
+				// be yes.
+				if (w || d || h) { pError = true; }
+
+				// Apply the rules for parsing floating-point number values to the descriptor.
+				// If the result is less than zero, let error be yes. Otherwise, let density
+				// be the result.
+				if (floatVal < 0) { pError = true; } else { d = floatVal; }
 
 				// If the descriptor consists of a valid non-negative integer followed by
-				// a U+0077 LATIN SMALL LETTER W character
-				if (regexNonNegativeInteger.test(value) && (lastChar === "w")) {
-
-					// If width and density are not both absent, then let error be yes.
-					if (w || d) { pError = true; }
-
-					// Apply the rules for parsing non-negative integers to the descriptor.
-					// If the result is zero, let error be yes.
-					// Otherwise, let width be the result.
-					if (intVal === 0) { pError = true; } else { w = intVal; }
-
-					// If the descriptor consists of a valid floating-point number followed by
-					// a U+0078 LATIN SMALL LETTER X character
-				} else if (regexFloatingPoint.test(value) && (lastChar === "x")) {
-
-					// If width, density and future-compat-h are not all absent, then let error
-					// be yes.
-					if (w || d || h) { pError = true; }
-
-					// Apply the rules for parsing floating-point number values to the descriptor.
-					// If the result is less than zero, let error be yes. Otherwise, let density
-					// be the result.
-					if (floatVal < 0) { pError = true; } else { d = floatVal; }
-
-					// If the descriptor consists of a valid non-negative integer followed by
-					// a U+0068 LATIN SMALL LETTER H character
-				} else if (regexNonNegativeInteger.test(value) && (lastChar === "h")) {
-
-					// If height and density are not both absent, then let error be yes.
-					if (h || d) { pError = true; }
-
-					// Apply the rules for parsing non-negative integers to the descriptor.
-					// If the result is zero, let error be yes. Otherwise, let future-compat-h
-					// be the result.
-					if (intVal === 0) { pError = true; } else { h = intVal; }
-
-					// Anything else, Let error be yes.
-				} else { pError = true; }
-			} // (close step 13 for loop)
-
-			// 15. If error is still no, then append a new image source to candidates whose
-			// URL is url, associated with a width width if not absent and a pixel
-			// density density if not absent. Otherwise, there is a parse error.
-			if (!pError) {
-				candidate.url = url;
-				if (w) { candidate.w = w; }
-				if (d) { candidate.d = d; }
-				if (h) { candidate.h = h; }
-				candidates.push(candidate);
-			} else if (console && console.log) {  // eslint-disable-line no-console
-				console.log("Invalid srcset descriptor found in \"" + input + "\" at \"" + desc + "\"."); // eslint-disable-line no-console
-			}
-		} // (close parseDescriptors fn)
-
-	}
+				// a U+0068 LATIN SMALL LETTER H character
+			} else if (regexNonNegativeInteger.test(value) && (lastChar === "h")) {
+
+				// If height and density are not both absent, then let error be yes.
+				if (h || d) { pError = true; }
+
+				// Apply the rules for parsing non-negative integers to the descriptor.
+				// If the result is zero, let error be yes. Otherwise, let future-compat-h
+				// be the result.
+				if (intVal === 0) { pError = true; } else { h = intVal; }
+
+				// Anything else, Let error be yes.
+			} else { pError = true; }
+		} // (close step 13 for loop)
+
+		// 15. If error is still no, then append a new image source to candidates whose
+		// URL is url, associated with a width width if not absent and a pixel
+		// density density if not absent. Otherwise, there is a parse error.
+		if (!pError) {
+			candidate.url = url;
+			if (w) { candidate.w = w; }
+			if (d) { candidate.d = d; }
+			if (h) { candidate.h = h; }
+			candidates.push(candidate);
+		} else if (console && console.log) {  // eslint-disable-line no-console
+			console.log("Invalid srcset descriptor found in \"" + input + "\" at \"" + desc + "\"."); // eslint-disable-line no-console
+		}
+	} // (close parseDescriptors fn)
 
-})();
+}

+ 40 - 0
lib/single-file/vendor/index.js

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2020 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * This file is part of SingleFile.
+ *
+ *   The code in this file is free software: you can redistribute it and/or 
+ *   modify it under the terms of the GNU Affero General Public License 
+ *   (GNU AGPL) as published by the Free Software Foundation, either version 3
+ *   of the License, or (at your option) any later version.
+ * 
+ *   The code in this file is distributed in the hope that it will be useful, 
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of 
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero 
+ *   General Public License for more details.
+ *
+ *   As additional permission under GNU AGPL version 3 section 7, you may 
+ *   distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU 
+ *   AGPL normally required by section 4, provided you include this license 
+ *   notice and a URL through which recipients can access the Corresponding 
+ *   Source.
+ */
+
+import * as fontPropertyParser from "./css-font-property-parser.js";
+import * as mediaQueryParser from "./css-media-query-parser.js";
+import * as cssMinifier from "./css-minifier.js";
+import * as cssTree from "./css-tree.js";
+import * as cssUnescape from "./css-unescape.js";
+import * as srcsetParser from "./html-srcset-parser";
+import { MIMEType } from "./mime-type-parser";
+
+export {
+	fontPropertyParser,
+	mediaQueryParser,
+	cssMinifier,
+	cssTree,
+	cssUnescape,
+	srcsetParser,
+	MIMEType
+};

+ 307 - 309
lib/single-file/vendor/mime-type-parser.js

@@ -46,405 +46,403 @@
  * SOFTWARE.
  */
 
-this.singlefile.lib.vendor.MIMEType = this.singlefile.lib.vendor.MIMEType || (() => {
+"use strict";
 
-	"use strict";
+let utils, parser, serializer, MIMEType;
 
-	let utils, parser, serializer, MIMEType;
+// lib/utils.js
+{
+	utils = {};
+	utils.removeLeadingAndTrailingHTTPWhitespace = string => {
+		return string.replace(/^[ \t\n\r]+/, "").replace(/[ \t\n\r]+$/, "");
+	};
 
-	// lib/utils.js
-	{
-		utils = {};
-		utils.removeLeadingAndTrailingHTTPWhitespace = string => {
-			return string.replace(/^[ \t\n\r]+/, "").replace(/[ \t\n\r]+$/, "");
-		};
+	utils.removeTrailingHTTPWhitespace = string => {
+		return string.replace(/[ \t\n\r]+$/, "");
+	};
 
-		utils.removeTrailingHTTPWhitespace = string => {
-			return string.replace(/[ \t\n\r]+$/, "");
-		};
+	utils.isHTTPWhitespaceChar = char => {
+		return char === " " || char === "\t" || char === "\n" || char === "\r";
+	};
 
-		utils.isHTTPWhitespaceChar = char => {
-			return char === " " || char === "\t" || char === "\n" || char === "\r";
-		};
+	utils.solelyContainsHTTPTokenCodePoints = string => {
+		return /^[-!#$%&'*+.^_`|~A-Za-z0-9]*$/.test(string);
+	};
 
-		utils.solelyContainsHTTPTokenCodePoints = string => {
-			return /^[-!#$%&'*+.^_`|~A-Za-z0-9]*$/.test(string);
-		};
+	utils.soleyContainsHTTPQuotedStringTokenCodePoints = string => {
+		return /^[\t\u0020-\u007E\u0080-\u00FF]*$/.test(string);
+	};
 
-		utils.soleyContainsHTTPQuotedStringTokenCodePoints = string => {
-			return /^[\t\u0020-\u007E\u0080-\u00FF]*$/.test(string);
-		};
+	utils.asciiLowercase = string => {
+		return string.replace(/[A-Z]/g, l => l.toLowerCase());
+	};
 
-		utils.asciiLowercase = string => {
-			return string.replace(/[A-Z]/g, l => l.toLowerCase());
-		};
+	// This variant only implements it with the extract-value flag set.
+	utils.collectAnHTTPQuotedString = (input, position) => {
+		let value = "";
 
-		// This variant only implements it with the extract-value flag set.
-		utils.collectAnHTTPQuotedString = (input, position) => {
-			let value = "";
+		position++;
 
-			position++;
+		// eslint-disable-next-line no-constant-condition
+		while (true) {
+			while (position < input.length && input[position] !== "\"" && input[position] !== "\\") {
+				value += input[position];
+				++position;
+			}
 
-			// eslint-disable-next-line no-constant-condition
-			while (true) {
-				while (position < input.length && input[position] !== "\"" && input[position] !== "\\") {
-					value += input[position];
-					++position;
-				}
+			if (position >= input.length) {
+				break;
+			}
+
+			const quoteOrBackslash = input[position];
+			++position;
 
+			if (quoteOrBackslash === "\\") {
 				if (position >= input.length) {
+					value += "\\";
 					break;
 				}
 
-				const quoteOrBackslash = input[position];
+				value += input[position];
 				++position;
+			} else {
+				break;
+			}
+		}
 
-				if (quoteOrBackslash === "\\") {
-					if (position >= input.length) {
-						value += "\\";
-						break;
-					}
+		return [value, position];
+	};
+}
 
-					value += input[position];
-					++position;
-				} else {
-					break;
-				}
-			}
+// lib/serializer.js
+{
+	const { solelyContainsHTTPTokenCodePoints } = utils;
+	serializer = mimeType => {
+		let serialization = `${mimeType.type}/${mimeType.subtype}`;
 
-			return [value, position];
-		};
-	}
+		if (mimeType.parameters.size === 0) {
+			return serialization;
+		}
 
-	// lib/serializer.js
-	{
-		const { solelyContainsHTTPTokenCodePoints } = utils;
-		serializer = mimeType => {
-			let serialization = `${mimeType.type}/${mimeType.subtype}`;
+		for (let [name, value] of mimeType.parameters) {
+			serialization += ";";
+			serialization += name;
+			serialization += "=";
 
-			if (mimeType.parameters.size === 0) {
-				return serialization;
+			if (!solelyContainsHTTPTokenCodePoints(value) || value.length === 0) {
+				value = value.replace(/(["\\])/g, "\\$1");
+				value = `"${value}"`;
 			}
 
-			for (let [name, value] of mimeType.parameters) {
-				serialization += ";";
-				serialization += name;
-				serialization += "=";
+			serialization += value;
+		}
 
-				if (!solelyContainsHTTPTokenCodePoints(value) || value.length === 0) {
-					value = value.replace(/(["\\])/g, "\\$1");
-					value = `"${value}"`;
-				}
+		return serialization;
+	};
+}
+
+// lib/parser.js
+{
+	const {
+		removeLeadingAndTrailingHTTPWhitespace,
+		removeTrailingHTTPWhitespace,
+		isHTTPWhitespaceChar,
+		solelyContainsHTTPTokenCodePoints,
+		soleyContainsHTTPQuotedStringTokenCodePoints,
+		asciiLowercase,
+		collectAnHTTPQuotedString
+	} = utils;
+
+	parser = input => {
+		input = removeLeadingAndTrailingHTTPWhitespace(input);
+
+		let position = 0;
+		let type = "";
+		while (position < input.length && input[position] !== "/") {
+			type += input[position];
+			++position;
+		}
 
-				serialization += value;
-			}
+		if (type.length === 0 || !solelyContainsHTTPTokenCodePoints(type)) {
+			return null;
+		}
 
-			return serialization;
-		};
-	}
+		if (position >= input.length) {
+			return null;
+		}
 
-	// lib/parser.js
-	{
-		const {
-			removeLeadingAndTrailingHTTPWhitespace,
-			removeTrailingHTTPWhitespace,
-			isHTTPWhitespaceChar,
-			solelyContainsHTTPTokenCodePoints,
-			soleyContainsHTTPQuotedStringTokenCodePoints,
-			asciiLowercase,
-			collectAnHTTPQuotedString
-		} = utils;
-
-		parser = input => {
-			input = removeLeadingAndTrailingHTTPWhitespace(input);
-
-			let position = 0;
-			let type = "";
-			while (position < input.length && input[position] !== "/") {
-				type += input[position];
-				++position;
-			}
+		// Skips past "/"
+		++position;
 
-			if (type.length === 0 || !solelyContainsHTTPTokenCodePoints(type)) {
-				return null;
-			}
+		let subtype = "";
+		while (position < input.length && input[position] !== ";") {
+			subtype += input[position];
+			++position;
+		}
 
-			if (position >= input.length) {
-				return null;
-			}
+		subtype = removeTrailingHTTPWhitespace(subtype);
+
+		if (subtype.length === 0 || !solelyContainsHTTPTokenCodePoints(subtype)) {
+			return null;
+		}
 
-			// Skips past "/"
+		const mimeType = {
+			type: asciiLowercase(type),
+			subtype: asciiLowercase(subtype),
+			parameters: new Map()
+		};
+
+		while (position < input.length) {
+			// Skip past ";"
 			++position;
 
-			let subtype = "";
-			while (position < input.length && input[position] !== ";") {
-				subtype += input[position];
+			while (isHTTPWhitespaceChar(input[position])) {
 				++position;
 			}
 
-			subtype = removeTrailingHTTPWhitespace(subtype);
-
-			if (subtype.length === 0 || !solelyContainsHTTPTokenCodePoints(subtype)) {
-				return null;
+			let parameterName = "";
+			while (position < input.length && input[position] !== ";" && input[position] !== "=") {
+				parameterName += input[position];
+				++position;
 			}
+			parameterName = asciiLowercase(parameterName);
 
-			const mimeType = {
-				type: asciiLowercase(type),
-				subtype: asciiLowercase(subtype),
-				parameters: new Map()
-			};
+			if (position < input.length) {
+				if (input[position] === ";") {
+					continue;
+				}
 
-			while (position < input.length) {
-				// Skip past ";"
+				// Skip past "="
 				++position;
+			}
 
-				while (isHTTPWhitespaceChar(input[position])) {
-					++position;
-				}
+			let parameterValue = null;
+			if (input[position] === "\"") {
+				[parameterValue, position] = collectAnHTTPQuotedString(input, position);
 
-				let parameterName = "";
-				while (position < input.length && input[position] !== ";" && input[position] !== "=") {
-					parameterName += input[position];
+				while (position < input.length && input[position] !== ";") {
 					++position;
 				}
-				parameterName = asciiLowercase(parameterName);
-
-				if (position < input.length) {
-					if (input[position] === ";") {
-						continue;
-					}
-
-					// Skip past "="
+			} else {
+				parameterValue = "";
+				while (position < input.length && input[position] !== ";") {
+					parameterValue += input[position];
 					++position;
 				}
 
-				let parameterValue = null;
-				if (input[position] === "\"") {
-					[parameterValue, position] = collectAnHTTPQuotedString(input, position);
+				parameterValue = removeTrailingHTTPWhitespace(parameterValue);
 
-					while (position < input.length && input[position] !== ";") {
-						++position;
-					}
-				} else {
-					parameterValue = "";
-					while (position < input.length && input[position] !== ";") {
-						parameterValue += input[position];
-						++position;
-					}
-
-					parameterValue = removeTrailingHTTPWhitespace(parameterValue);
-
-					if (parameterValue === "") {
-						continue;
-					}
-				}
-
-				if (parameterName.length > 0 &&
-					solelyContainsHTTPTokenCodePoints(parameterName) &&
-					soleyContainsHTTPQuotedStringTokenCodePoints(parameterValue) &&
-					!mimeType.parameters.has(parameterName)) {
-					mimeType.parameters.set(parameterName, parameterValue);
+				if (parameterValue === "") {
+					continue;
 				}
 			}
 
-			return mimeType;
-		};
-	}
-
-	// lib/mime-type.js
-	{		
-		const parse = parser;
-		const serialize = serializer;
-		const {
-			asciiLowercase,
-			solelyContainsHTTPTokenCodePoints,
-			soleyContainsHTTPQuotedStringTokenCodePoints
-		} = utils;
-
-		MIMEType = class MIMEType {
-			constructor(string) {
-				string = String(string);
-				const result = parse(string);
-				if (result === null) {
-					throw new Error(`Could not parse MIME type string "${string}"`);
-				}
-
-				this._type = result.type;
-				this._subtype = result.subtype;
-				this._parameters = new MIMETypeParameters(result.parameters);
+			if (parameterName.length > 0 &&
+				solelyContainsHTTPTokenCodePoints(parameterName) &&
+				soleyContainsHTTPQuotedStringTokenCodePoints(parameterValue) &&
+				!mimeType.parameters.has(parameterName)) {
+				mimeType.parameters.set(parameterName, parameterValue);
 			}
+		}
 
-			static parse(string) {
-				try {
-					return new this(string);
-				} catch (e) {
-					return null;
-				}
-			}
+		return mimeType;
+	};
+}
+
+// lib/mime-type.js
+{
+	const parse = parser;
+	const serialize = serializer;
+	const {
+		asciiLowercase,
+		solelyContainsHTTPTokenCodePoints,
+		soleyContainsHTTPQuotedStringTokenCodePoints
+	} = utils;
+
+	MIMEType = class MIMEType {
+		constructor(string) {
+			string = String(string);
+			const result = parse(string);
+			if (result === null) {
+				throw new Error(`Could not parse MIME type string "${string}"`);
+			}
+
+			this._type = result.type;
+			this._subtype = result.subtype;
+			this._parameters = new MIMETypeParameters(result.parameters);
+		}
 
-			get essence() {
-				return `${this.type}/${this.subtype}`;
+		static parse(string) {
+			try {
+				return new this(string);
+			} catch (e) {
+				return null;
 			}
+		}
 
-			get type() {
-				return this._type;
-			}
+		get essence() {
+			return `${this.type}/${this.subtype}`;
+		}
 
-			set type(value) {
-				value = asciiLowercase(String(value));
+		get type() {
+			return this._type;
+		}
 
-				if (value.length === 0) {
-					throw new Error("Invalid type: must be a non-empty string");
-				}
-				if (!solelyContainsHTTPTokenCodePoints(value)) {
-					throw new Error(`Invalid type ${value}: must contain only HTTP token code points`);
-				}
+		set type(value) {
+			value = asciiLowercase(String(value));
 
-				this._type = value;
+			if (value.length === 0) {
+				throw new Error("Invalid type: must be a non-empty string");
 			}
-
-			get subtype() {
-				return this._subtype;
+			if (!solelyContainsHTTPTokenCodePoints(value)) {
+				throw new Error(`Invalid type ${value}: must contain only HTTP token code points`);
 			}
 
-			set subtype(value) {
-				value = asciiLowercase(String(value));
+			this._type = value;
+		}
 
-				if (value.length === 0) {
-					throw new Error("Invalid subtype: must be a non-empty string");
-				}
-				if (!solelyContainsHTTPTokenCodePoints(value)) {
-					throw new Error(`Invalid subtype ${value}: must contain only HTTP token code points`);
-				}
+		get subtype() {
+			return this._subtype;
+		}
 
-				this._subtype = value;
-			}
+		set subtype(value) {
+			value = asciiLowercase(String(value));
 
-			get parameters() {
-				return this._parameters;
+			if (value.length === 0) {
+				throw new Error("Invalid subtype: must be a non-empty string");
 			}
-
-			toString() {
-				// The serialize function works on both "MIME type records" (i.e. the results of parse) and on this class, since
-				// this class's interface is identical.
-				return serialize(this);
+			if (!solelyContainsHTTPTokenCodePoints(value)) {
+				throw new Error(`Invalid subtype ${value}: must contain only HTTP token code points`);
 			}
 
-			isJavaScript({ allowParameters = false } = {}) {
-				switch (this._type) {
-					case "text": {
-						switch (this._subtype) {
-							case "ecmascript":
-							case "javascript":
-							case "javascript1.0":
-							case "javascript1.1":
-							case "javascript1.2":
-							case "javascript1.3":
-							case "javascript1.4":
-							case "javascript1.5":
-							case "jscript":
-							case "livescript":
-							case "x-ecmascript":
-							case "x-javascript": {
-								return allowParameters || this._parameters.size === 0;
-							}
-							default: {
-								return false;
-							}
+			this._subtype = value;
+		}
+
+		get parameters() {
+			return this._parameters;
+		}
+
+		toString() {
+			// The serialize function works on both "MIME type records" (i.e. the results of parse) and on this class, since
+			// this class's interface is identical.
+			return serialize(this);
+		}
+
+		isJavaScript({ allowParameters = false } = {}) {
+			switch (this._type) {
+				case "text": {
+					switch (this._subtype) {
+						case "ecmascript":
+						case "javascript":
+						case "javascript1.0":
+						case "javascript1.1":
+						case "javascript1.2":
+						case "javascript1.3":
+						case "javascript1.4":
+						case "javascript1.5":
+						case "jscript":
+						case "livescript":
+						case "x-ecmascript":
+						case "x-javascript": {
+							return allowParameters || this._parameters.size === 0;
 						}
-					}
-					case "application": {
-						switch (this._subtype) {
-							case "ecmascript":
-							case "javascript":
-							case "x-ecmascript":
-							case "x-javascript": {
-								return allowParameters || this._parameters.size === 0;
-							}
-							default: {
-								return false;
-							}
+						default: {
+							return false;
 						}
 					}
-					default: {
-						return false;
+				}
+				case "application": {
+					switch (this._subtype) {
+						case "ecmascript":
+						case "javascript":
+						case "x-ecmascript":
+						case "x-javascript": {
+							return allowParameters || this._parameters.size === 0;
+						}
+						default: {
+							return false;
+						}
 					}
 				}
+				default: {
+					return false;
+				}
 			}
-			isXML() {
-				return (this._subtype === "xml" && (this._type === "text" || this._type === "application")) ||
-					this._subtype.endsWith("+xml");
-			}
-			isHTML() {
-				return this._subtype === "html" && this._type === "text";
-			}
-		};
-
-		class MIMETypeParameters {
-			constructor(map) {
-				this._map = map;
-			}
+		}
+		isXML() {
+			return (this._subtype === "xml" && (this._type === "text" || this._type === "application")) ||
+				this._subtype.endsWith("+xml");
+		}
+		isHTML() {
+			return this._subtype === "html" && this._type === "text";
+		}
+	};
 
-			get size() {
-				return this._map.size;
-			}
+	class MIMETypeParameters {
+		constructor(map) {
+			this._map = map;
+		}
 
-			get(name) {
-				name = asciiLowercase(String(name));
-				return this._map.get(name);
-			}
+		get size() {
+			return this._map.size;
+		}
 
-			has(name) {
-				name = asciiLowercase(String(name));
-				return this._map.has(name);
-			}
+		get(name) {
+			name = asciiLowercase(String(name));
+			return this._map.get(name);
+		}
 
-			set(name, value) {
-				name = asciiLowercase(String(name));
-				value = String(value);
+		has(name) {
+			name = asciiLowercase(String(name));
+			return this._map.has(name);
+		}
 
-				if (!solelyContainsHTTPTokenCodePoints(name)) {
-					throw new Error(`Invalid MIME type parameter name "${name}": only HTTP token code points are valid.`);
-				}
-				if (!soleyContainsHTTPQuotedStringTokenCodePoints(value)) {
-					throw new Error(`Invalid MIME type parameter value "${value}": only HTTP quoted-string token code points are valid.`);
-				}
+		set(name, value) {
+			name = asciiLowercase(String(name));
+			value = String(value);
 
-				return this._map.set(name, value);
+			if (!solelyContainsHTTPTokenCodePoints(name)) {
+				throw new Error(`Invalid MIME type parameter name "${name}": only HTTP token code points are valid.`);
 			}
-
-			clear() {
-				this._map.clear();
+			if (!soleyContainsHTTPQuotedStringTokenCodePoints(value)) {
+				throw new Error(`Invalid MIME type parameter value "${value}": only HTTP quoted-string token code points are valid.`);
 			}
 
-			delete(name) {
-				name = asciiLowercase(String(name));
-				return this._map.delete(name);
-			}
+			return this._map.set(name, value);
+		}
 
-			forEach(callbackFn, thisArg) {
-				this._map.forEach(callbackFn, thisArg);
-			}
+		clear() {
+			this._map.clear();
+		}
 
-			keys() {
-				return this._map.keys();
-			}
+		delete(name) {
+			name = asciiLowercase(String(name));
+			return this._map.delete(name);
+		}
 
-			values() {
-				return this._map.values();
-			}
+		forEach(callbackFn, thisArg) {
+			this._map.forEach(callbackFn, thisArg);
+		}
 
-			entries() {
-				return this._map.entries();
-			}
+		keys() {
+			return this._map.keys();
+		}
 
-			[Symbol.iterator]() {
-				return this._map[Symbol.iterator]();
-			}
+		values() {
+			return this._map.values();
+		}
+
+		entries() {
+			return this._map.entries();
 		}
 
+		[Symbol.iterator]() {
+			return this._map[Symbol.iterator]();
+		}
 	}
 
-	return MIMEType;
+}
 
-})();
+export {
+	MIMEType
+};

+ 7 - 35
manifest.json

@@ -17,14 +17,8 @@
 			],
 			"run_at": "document_start",
 			"js": [
-				"lib/single-file/index.js",
-				"extension/index.js",
-				"extension/lib/single-file/index.js",
 				"extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js",
-				"lib/single-file/single-file-helper.js",
-				"lib/single-file/vendor/css-unescape.js",
-				"lib/single-file/processors/hooks/content/content-hooks-frames.js",
-				"lib/single-file/processors/frame-tree/content/content-frame-tree.js"
+				"lib/single-file/dist/single-file-frames.js"				
 			],
 			"all_frames": true,
 			"match_about_blank": true
@@ -43,11 +37,8 @@
 			],
 			"run_at": "document_start",
 			"js": [
-				"lib/single-file/index.js",
-				"lib/single-file/processors/hooks/content/content-hooks.js",
-				"lib/single-file/modules/html-serializer.js",
+				"lib/single-file/dist/single-file-bootstrap.js",
 				"extension/index.js",
-				"extension/lib/single-file/index.js",
 				"extension/core/index.js",
 				"extension/core/content/content-bootstrap.js"
 			]
@@ -55,32 +46,14 @@
 	],
 	"background": {
 		"scripts": [
-			"lib/single-file/index.js",
+			"extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js",
+			"lib/single-file/dist/single-file.js",
+			"common/index.js",
+			"common/ui/content/content-infobar.js",
 			"extension/index.js",
 			"extension/lib/single-file/index.js",
 			"extension/core/index.js",
 			"extension/ui/index.js",
-			"extension/lib/single-file/browser-polyfill/chrome-browser-polyfill.js",
-			"lib/single-file/vendor/css-minifier.js",
-			"lib/single-file/vendor/css-tree.js",
-			"lib/single-file/vendor/css-media-query-parser.js",
-			"lib/single-file/vendor/html-srcset-parser.js",
-			"lib/single-file/vendor/css-font-property-parser.js",
-			"lib/single-file/vendor/css-unescape.js",
-			"lib/single-file/vendor/mime-type-parser.js",
-			"lib/single-file/single-file-util.js",
-			"lib/single-file/single-file-helper.js",
-			"lib/single-file/modules/css-fonts-minifier.js",
-			"lib/single-file/modules/css-fonts-alt-minifier.js",
-			"lib/single-file/modules/css-medias-alt-minifier.js",
-			"lib/single-file/modules/css-matched-rules.js",
-			"lib/single-file/modules/css-rules-minifier.js",
-			"lib/single-file/modules/html-minifier.js",
-			"lib/single-file/modules/html-serializer.js",
-			"lib/single-file/modules/html-images-alt-minifier.js",
-			"lib/single-file/single-file-core.js",			
-			"common/index.js",
-			"common/ui/content/content-infobar.js",
 			"extension/lib/single-file/core/bg/scripts.js",
 			"extension/lib/single-file/fetch/content/content-fetch.js",
 			"extension/lib/single-file/fetch/bg/fetch.js",
@@ -144,8 +117,7 @@
 		}
 	},
 	"web_accessible_resources": [
-		"lib/single-file/index.js",
-		"lib/single-file/modules/html-serializer.js",
+		"lib/single-file/dist/single-file.js",
 		"lib/single-file/processors/hooks/content/content-hooks-web.js",
 		"lib/single-file/processors/hooks/content/content-hooks-frames-web.js",
 		"common/ui/content/content-infobar-web.js",

Some files were not shown because too many files changed in this diff