htmlnano.js 5.9 KB

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