html-minifier.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. /*
  2. * Copyright 2010-2019 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * The MIT License (MIT)
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in all
  15. * copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. * SOFTWARE.
  24. */
  25. // Derived from the work of Kirill Maltsev - https://github.com/posthtml/htmlnano
  26. this.htmlMinifier = this.htmlMinifier || (() => {
  27. // Source: https://github.com/kangax/html-minifier/issues/63
  28. const booleanAttributes = [
  29. "allowfullscreen",
  30. "async",
  31. "autofocus",
  32. "autoplay",
  33. "checked",
  34. "compact",
  35. "controls",
  36. "declare",
  37. "default",
  38. "defaultchecked",
  39. "defaultmuted",
  40. "defaultselected",
  41. "defer",
  42. "disabled",
  43. "enabled",
  44. "formnovalidate",
  45. "hidden",
  46. "indeterminate",
  47. "inert",
  48. "ismap",
  49. "itemscope",
  50. "loop",
  51. "multiple",
  52. "muted",
  53. "nohref",
  54. "noresize",
  55. "noshade",
  56. "novalidate",
  57. "nowrap",
  58. "open",
  59. "pauseonexit",
  60. "readonly",
  61. "required",
  62. "reversed",
  63. "scoped",
  64. "seamless",
  65. "selected",
  66. "sortable",
  67. "truespeed",
  68. "typemustmatch",
  69. "visible"
  70. ];
  71. const noWhitespaceCollapseElements = ["script", "style", "pre", "textarea"];
  72. // Source: https://www.w3.org/TR/html4/sgml/dtd.html#events (Generic Attributes)
  73. const safeToRemoveAttrs = [
  74. "id",
  75. "class",
  76. "style",
  77. "lang",
  78. "dir",
  79. "onclick",
  80. "ondblclick",
  81. "onmousedown",
  82. "onmouseup",
  83. "onmouseover",
  84. "onmousemove",
  85. "onmouseout",
  86. "onkeypress",
  87. "onkeydown",
  88. "onkeyup"
  89. ];
  90. const redundantAttributes = {
  91. "form": {
  92. "method": "get"
  93. },
  94. "script": {
  95. "language": "javascript",
  96. "type": "text/javascript",
  97. // Remove attribute if the function returns false
  98. "charset": node => {
  99. // The charset attribute only really makes sense on “external” SCRIPT elements:
  100. // http://perfectionkills.com/optimizing-html/#8_script_charset
  101. return !node.getAttribute("src");
  102. }
  103. },
  104. "style": {
  105. "media": "all",
  106. "type": "text/css"
  107. },
  108. "link": {
  109. "media": "all"
  110. }
  111. };
  112. const REGEXP_WHITESPACE = /[ \t\f\r]+/g;
  113. const REGEXP_NEWLINE = /[\n]+/g;
  114. const REGEXP_ENDS_WHITESPACE = /^\s+$/;
  115. const NodeFilter_SHOW_ALL = 4294967295;
  116. const Node_ELEMENT_NODE = 1;
  117. const Node_TEXT_NODE = 3;
  118. const Node_COMMENT_NODE = 8;
  119. const modules = [
  120. collapseBooleanAttributes,
  121. mergeTextNodes,
  122. collapseWhitespace,
  123. removeComments,
  124. removeEmptyAttributes,
  125. removeRedundantAttributes,
  126. compressJSONLD,
  127. node => mergeElements(node, "style", (node, previousSibling) => node.parentElement && node.parentElement.tagName == "HEAD" && node.media == previousSibling.media && node.title == previousSibling.title)
  128. ];
  129. return {
  130. process: (doc, options) => {
  131. removeEmptyInlineElements(doc);
  132. const nodesWalker = doc.createTreeWalker(doc.documentElement, NodeFilter_SHOW_ALL, null, false);
  133. let node = nodesWalker.nextNode();
  134. while (node) {
  135. const deletedNode = modules.find(module => module(node, options));
  136. const previousNode = node;
  137. node = nodesWalker.nextNode();
  138. if (deletedNode) {
  139. previousNode.remove();
  140. }
  141. }
  142. }
  143. };
  144. function collapseBooleanAttributes(node) {
  145. if (node.nodeType == Node_ELEMENT_NODE) {
  146. Array.from(node.attributes).forEach(attribute => {
  147. if (booleanAttributes.includes(attribute.name)) {
  148. node.setAttribute(attribute.name, "");
  149. }
  150. });
  151. }
  152. }
  153. function mergeTextNodes(node) {
  154. if (node.nodeType == Node_TEXT_NODE) {
  155. if (node.previousSibling && node.previousSibling.nodeType == Node_TEXT_NODE) {
  156. node.textContent = node.previousSibling.textContent + node.textContent;
  157. node.previousSibling.remove();
  158. }
  159. }
  160. }
  161. function mergeElements(node, tagName, acceptMerge) {
  162. if (node.nodeType == Node_ELEMENT_NODE && node.tagName.toLowerCase() == tagName.toLowerCase()) {
  163. let previousSibling = node.previousSibling;
  164. const previousSiblings = [];
  165. while (previousSibling && previousSibling.nodeType == Node_TEXT_NODE && !previousSibling.textContent.trim()) {
  166. previousSiblings.push(previousSibling);
  167. previousSibling = previousSibling.previousSibling;
  168. }
  169. if (previousSibling && previousSibling.nodeType == Node_ELEMENT_NODE && previousSibling.tagName == node.tagName && acceptMerge(node, previousSibling)) {
  170. node.textContent = previousSibling.textContent + node.textContent;
  171. previousSiblings.forEach(node => node.remove());
  172. previousSibling.remove();
  173. }
  174. }
  175. }
  176. function collapseWhitespace(node, options) {
  177. if (node.nodeType == Node_TEXT_NODE) {
  178. let element = node.parentElement;
  179. const spacePreserved = element.getAttribute(options.preservedSpaceAttributeName) == "";
  180. const textContent = node.textContent;
  181. let noWhitespace = !spacePreserved && noWhitespaceCollapse(element);
  182. while (noWhitespace) {
  183. element = element.parentElement;
  184. noWhitespace = element && noWhitespaceCollapse(element);
  185. }
  186. if ((!element || noWhitespace) && textContent.length > 1) {
  187. node.textContent = textContent.replace(REGEXP_WHITESPACE, getWhiteSpace(node)).replace(REGEXP_NEWLINE, "\n");
  188. }
  189. }
  190. }
  191. function getWhiteSpace(node) {
  192. return node.parentElement && node.parentElement.tagName == "HEAD" ? "\n" : " ";
  193. }
  194. function noWhitespaceCollapse(element) {
  195. return element && !noWhitespaceCollapseElements.includes(element.tagName.toLowerCase());
  196. }
  197. function removeComments(node) {
  198. if (node.nodeType == Node_COMMENT_NODE) {
  199. return !node.textContent.toLowerCase().trim().startsWith("[if");
  200. }
  201. }
  202. function removeEmptyAttributes(node) {
  203. if (node.nodeType == Node_ELEMENT_NODE) {
  204. Array.from(node.attributes).forEach(attribute => {
  205. if (safeToRemoveAttrs.includes(attribute.name.toLowerCase())) {
  206. const attributeValue = node.getAttribute(attribute.name);
  207. if (attributeValue == "" || (attributeValue || "").match(REGEXP_ENDS_WHITESPACE)) {
  208. node.removeAttribute(attribute.name);
  209. }
  210. }
  211. });
  212. }
  213. }
  214. function removeRedundantAttributes(node) {
  215. if (node.nodeType == Node_ELEMENT_NODE) {
  216. const tagRedundantAttributes = redundantAttributes[node.tagName.toLowerCase()];
  217. if (tagRedundantAttributes) {
  218. Object.keys(tagRedundantAttributes).forEach(redundantAttributeName => {
  219. const tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
  220. if (typeof tagRedundantAttributeValue == "function" ? tagRedundantAttributeValue(node) : node.getAttribute(redundantAttributeName) == tagRedundantAttributeValue) {
  221. node.removeAttribute(redundantAttributeName);
  222. }
  223. });
  224. }
  225. }
  226. }
  227. function compressJSONLD(node) {
  228. if (node.nodeType == Node_ELEMENT_NODE && node.tagName == "SCRIPT" && node.type == "application/ld+json" && node.textContent.trim()) {
  229. try {
  230. node.textContent = JSON.stringify(JSON.parse(node.textContent));
  231. } catch (error) {
  232. /* ignored */
  233. }
  234. }
  235. }
  236. function removeEmptyInlineElements(doc) {
  237. doc.querySelectorAll("style, script:not([src]), noscript").forEach(element => {
  238. if (!element.textContent.trim()) {
  239. element.remove();
  240. }
  241. });
  242. }
  243. })();