1
0
Эх сурвалжийг харах

use htmlnano to compress HTML contents

Gildas 7 жил өмнө
parent
commit
dfabf3cbdb

+ 218 - 0
lib/single-file/htmlnano.js

@@ -0,0 +1,218 @@
+/*
+ * Copyright 2018 Gildas Lormeau
+ * contact : gildas.lormeau <at> gmail.com
+ * 
+ * The MIT License (MIT)
+ * 
+ * Copyright (c) 2018
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+// Derived from the work of Kirill Maltsev - https://github.com/posthtml/htmlnano
+
+/* global Node, NodeFilter */
+
+this.htmlnano = (() => {
+
+	// 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"
+	];
+
+	function collapseBooleanAttributes(node) {
+		if (node.nodeType == Node.ELEMENT_NODE) {
+			node.getAttributeNames().forEach(attributeName => {
+				if (booleanAttributes.includes(attributeName)) {
+					node.setAttribute(attributeName, "");
+				}
+			});
+		}
+	}
+
+	const noWhitespaceCollapseElements = ["script", "style", "pre", "textarea"];
+
+	/** Collapses redundant whitespaces */
+	function collapseWhitespace(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();
+			}
+			let element = node.parentElement;
+			let textContent = node.textContent;
+			while (noWhitespaceCollapse(element)) {
+				element = element.parentElement;
+			}
+			if ((!element || noWhitespaceCollapse(element)) && textContent.match(/\s+/) && textContent.length > 1) {
+				let lastTextContent;
+				while (lastTextContent != textContent) {
+					lastTextContent = textContent;
+					textContent = textContent.replace(/( )+|(\n)+|(\t)+|(\f)+||(\r)+/g, "$1");
+				}
+				node.textContent = textContent;
+			}
+		}
+	}
+
+	function noWhitespaceCollapse(element) {
+		return element && !noWhitespaceCollapseElements.includes(element.tagName.toLowerCase());
+	}
+
+	/** Removes HTML comments */
+	function removeComments(node) {
+		if (node.nodeType == Node.COMMENT_NODE) {
+			return !node.textContent.toLowerCase().trim().startsWith("[if");
+		}
+	}
+
+	// Source: https://www.w3.org/TR/html4/sgml/dtd.html#events (Generic Attributes)
+	const safeToRemoveAttrs = [
+		"id",
+		"class",
+		"style",
+		"title",
+		"lang",
+		"dir",
+		"onclick",
+		"ondblclick",
+		"onmousedown",
+		"onmouseup",
+		"onmouseover",
+		"onmousemove",
+		"onmouseout",
+		"onkeypress",
+		"onkeydown",
+		"onkeyup"
+	];
+
+	/** Removes empty attributes */
+	function removeEmptyAttributes(node) {
+		if (node.nodeType == Node.ELEMENT_NODE) {
+			node.getAttributeNames().forEach(attributeName => {
+				if (safeToRemoveAttrs.includes(attributeName.toLowerCase())) {
+					const attributeValue = node.getAttribute(attributeName);
+					if (attributeValue === "" || (attributeValue || "").match(/^\s+$/)) {
+						node.removeAttribute(attributeName);
+					}
+				}
+			});
+		}
+	}
+
+	const redundantAttributes = {
+		"form": {
+			"method": "get"
+		},
+		"input": {
+			"type": "text"
+		},
+		"button": {
+			"type": "submit"
+		},
+		"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.attrs && !node.attrs.src;
+			}
+		},
+		"style": {
+			"media": "all",
+			"type": "text/css"
+		},
+		"link": {
+			"media": "all"
+		}
+	};
+
+	/** Removes redundant attributes */
+	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);
+					}
+				});
+			}
+		}
+	}
+
+	const modules = [collapseBooleanAttributes, collapseWhitespace, removeComments, removeEmptyAttributes, removeRedundantAttributes];
+
+	return 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));
+			const previousNode = node;
+			node = nodesWalker.nextNode();
+			if (deletedNode) {
+				previousNode.remove();
+			}
+		}
+	};
+
+})();

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

@@ -18,7 +18,7 @@
  *   along with SingleFile.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/* global SingleFileCore, base64, DOMParser, TextDecoder, fetch, superFetch, parseSrcset, uglifycss */
+/* global SingleFileCore, base64, DOMParser, TextDecoder, fetch, superFetch, parseSrcset, uglifycss, htmlnano */
 
 this.SingleFile = (() => {
 
@@ -94,7 +94,8 @@ this.SingleFile = (() => {
 				document: doc,
 				serialize: () => getDoctype(doc) + doc.documentElement.outerHTML,
 				parseSrcset: srcset => parseSrcset(srcset),
-				uglifycss: (content, options) => uglifycss.processString(content, options)
+				uglifycss: (content, options) => uglifycss.processString(content, options),
+				htmlnano: doc => htmlnano(doc)
 			};
 		}
 	}

+ 1 - 21
lib/single-file/single-file-core.js

@@ -375,27 +375,7 @@ this.SingleFileCore = (() => {
 		}
 
 		compressHTML() {
-			const textNodesWalker = this.doc.createTreeWalker(this.doc.documentElement, 4, null, false);
-			let node = textNodesWalker.nextNode();
-			while (node) {
-				let element = node.parentElement;
-				while (element && element.tagName != "PRE") {
-					element = element.parentElement;
-				}
-				if (!element) {
-					node.textContent = node.textContent.replace(/ +/g, " ");
-					node.textContent = node.textContent.replace(/\n+/g, " ");
-				}
-				node = textNodesWalker.nextNode();
-			}
-			const commentNodesWalker = this.doc.createTreeWalker(this.doc.documentElement, 128, null, false);
-			node = commentNodesWalker.nextNode();
-			let removedNodes = [];
-			while (node) {
-				removedNodes.push(node);
-				node = commentNodesWalker.nextNode();
-			}
-			removedNodes.forEach(node => node.remove());
+			this.dom.htmlnano(this.doc);
 		}
 
 		insertSingleFileCommentNode() {

+ 1 - 0
manifest.json

@@ -39,6 +39,7 @@
                 "extension/index.js",
                 "extension/ui/content/ui.js",
                 "lib/single-file/base64.js",
+                "lib/single-file/htmlnano.js",
                 "lib/single-file/uglifycss.js",
                 "lib/single-file/parse-srcset.js",
                 "lib/single-file/single-file-core.js",