rules-minifier.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. /*
  2. * Copyright 2018 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * This file is part of SingleFile.
  6. *
  7. * SingleFile is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Lesser General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * SingleFile is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public License
  18. * along with SingleFile. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. /* global CSSRule */
  21. this.rulesMinifier = this.rulesMinifier || (() => {
  22. const REGEXP_PSEUDO_CLASSES = /::after|::before|::first-line|::first-letter|:focus|:focus-within|:hover|:link|:visited|:active/gi;
  23. return {
  24. process: doc => {
  25. const rulesData = {
  26. selectors: new Set(),
  27. fonts: {
  28. declared: new Set(),
  29. used: new Set()
  30. }
  31. };
  32. const stats = {
  33. processed: 0,
  34. discarded: 0
  35. };
  36. doc.querySelectorAll("style").forEach(style => {
  37. if (style.sheet) {
  38. const processed = style.sheet.cssRules.length;
  39. stats.processed += processed;
  40. style.textContent = processRules(doc, style.sheet.cssRules, rulesData);
  41. stats.discarded += processed - style.sheet.cssRules.length;
  42. }
  43. });
  44. const unusedFonts = Array.from(rulesData.fonts.declared).filter(fontFamily => !rulesData.fonts.used.has(fontFamily));
  45. doc.querySelectorAll("style").forEach(style => {
  46. if (style.sheet) {
  47. const processed = style.sheet.cssRules.length;
  48. style.textContent = deleteUnusedFonts(doc, style.sheet.cssRules, unusedFonts);
  49. stats.discarded += processed - style.sheet.cssRules.length;
  50. }
  51. });
  52. return stats;
  53. }
  54. };
  55. function processRules(doc, rules, rulesData) {
  56. let stylesheetContent = "";
  57. if (rules) {
  58. Array.from(rules).forEach(rule => {
  59. if (rule.media) {
  60. stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + " {";
  61. stylesheetContent += processRules(doc, rule.cssRules, rulesData);
  62. stylesheetContent += "}";
  63. } else if (rule.selectorText) {
  64. const selector = getFilteredSelector(rule.selectorText);
  65. if (selector) {
  66. try {
  67. if (rulesData.selectors.has(selector) || doc.querySelector(selector)) {
  68. stylesheetContent += rule.cssText;
  69. rulesData.selectors.add(selector);
  70. if (rule.style.fontFamily) {
  71. rule.style.fontFamily.split(",").forEach(fontFamily => rulesData.fonts.used.add(fontFamily.trim()));
  72. }
  73. }
  74. } catch (error) {
  75. stylesheetContent += rule.cssText;
  76. }
  77. }
  78. } else {
  79. if (rule.type == CSSRule.FONT_FACE_RULE) {
  80. rulesData.fonts.declared.add(rule.style.fontFamily.trim());
  81. }
  82. stylesheetContent += rule.cssText;
  83. }
  84. });
  85. }
  86. return stylesheetContent;
  87. }
  88. function deleteUnusedFonts(doc, rules, unusedFonts) {
  89. let stylesheetContent = "";
  90. if (rules) {
  91. Array.from(rules).forEach(rule => {
  92. if (rule.media) {
  93. stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + " {";
  94. stylesheetContent += deleteUnusedFonts(doc, rule.cssRules, unusedFonts);
  95. stylesheetContent += "}";
  96. } else if (rule.selectorText) {
  97. stylesheetContent += rule.cssText;
  98. } else if (rule.type == CSSRule.FONT_FACE_RULE && !unusedFonts.includes(rule.style.fontFamily.trim())) {
  99. stylesheetContent += rule.cssText;
  100. } else {
  101. stylesheetContent += rule.cssText;
  102. }
  103. });
  104. }
  105. return stylesheetContent;
  106. }
  107. function getFilteredSelector(selector) {
  108. if (selector.match(REGEXP_PSEUDO_CLASSES)) {
  109. let selectors = selector.split(/\s*,\s*/g);
  110. selector = selectors.map(selector => {
  111. const simpleSelectors = selector.split(/\s*[ >~+]\s*/g);
  112. const separators = selector.match(/\s*[ >~+]\s*/g);
  113. return simpleSelectors.map((selector, selectorIndex) => {
  114. while (selector.match(REGEXP_PSEUDO_CLASSES)) {
  115. selector = selector.replace(REGEXP_PSEUDO_CLASSES, "").trim();
  116. }
  117. selector = selector.replace(/:?:[^(]+\(\)/g, "");
  118. if (selector == "") {
  119. selector = "*";
  120. }
  121. return selector + (separators && separators[selectorIndex] ? separators[selectorIndex] : "");
  122. }).join("");
  123. }).join(",");
  124. }
  125. return selector;
  126. }
  127. })();