rules-minifier.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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. doc.querySelectorAll("[style]").forEach(element => {
  45. if (element.style.fontFamily) {
  46. element.style.fontFamily.split(",").forEach(fontFamily => rulesData.fonts.used.add(getFontFamily(fontFamily)));
  47. }
  48. });
  49. const unusedFonts = Array.from(rulesData.fonts.declared).filter(fontFamily => !rulesData.fonts.used.has(fontFamily));
  50. doc.querySelectorAll("style").forEach(style => {
  51. if (style.sheet) {
  52. const processed = style.sheet.cssRules.length;
  53. style.textContent = deleteUnusedFonts(doc, style.sheet.cssRules, unusedFonts);
  54. stats.discarded += processed - style.sheet.cssRules.length;
  55. }
  56. });
  57. return stats;
  58. }
  59. };
  60. function processRules(doc, rules, rulesData) {
  61. let stylesheetContent = "";
  62. if (rules) {
  63. Array.from(rules).forEach(rule => {
  64. if (rule.media) {
  65. stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + " {";
  66. stylesheetContent += processRules(doc, rule.cssRules, rulesData);
  67. stylesheetContent += "}";
  68. } else if (rule.selectorText) {
  69. const selector = getFilteredSelector(rule.selectorText);
  70. if (selector) {
  71. try {
  72. if (rulesData.selectors.has(selector) || doc.querySelector(selector)) {
  73. stylesheetContent += rule.cssText;
  74. rulesData.selectors.add(selector);
  75. if (rule.style && rule.style.fontFamily) {
  76. rule.style.fontFamily.split(",").forEach(fontFamily => rulesData.fonts.used.add(getFontFamily(fontFamily)));
  77. }
  78. }
  79. } catch (error) {
  80. stylesheetContent += rule.cssText;
  81. }
  82. }
  83. } else {
  84. if (rule.type == CSSRule.FONT_FACE_RULE && rule.style && rule.style.fontFamily) {
  85. rulesData.fonts.declared.add(getFontFamily(rule.style.fontFamily));
  86. }
  87. stylesheetContent += rule.cssText;
  88. }
  89. });
  90. }
  91. return stylesheetContent;
  92. }
  93. function deleteUnusedFonts(doc, rules, unusedFonts) {
  94. let stylesheetContent = "";
  95. if (rules) {
  96. Array.from(rules).forEach(rule => {
  97. if (rule.media) {
  98. stylesheetContent += "@media " + Array.prototype.join.call(rule.media, ",") + " {";
  99. stylesheetContent += deleteUnusedFonts(doc, rule.cssRules, unusedFonts);
  100. stylesheetContent += "}";
  101. } else if (rule.selectorText) {
  102. stylesheetContent += rule.cssText;
  103. } else if (rule.type == CSSRule.FONT_FACE_RULE && rule.style && rule.style.fontFamily && !unusedFonts.includes(getFontFamily(rule.style.fontFamily))) {
  104. stylesheetContent += rule.cssText;
  105. } else if (rule.type != CSSRule.FONT_FACE_RULE) {
  106. stylesheetContent += rule.cssText;
  107. }
  108. });
  109. }
  110. return stylesheetContent;
  111. }
  112. function getFilteredSelector(selector) {
  113. if (selector.match(REGEXP_PSEUDO_CLASSES)) {
  114. let selectors = selector.split(/\s*,\s*/g);
  115. selector = selectors.map(selector => {
  116. const simpleSelectors = selector.split(/\s*[ >~+]\s*/g);
  117. const separators = selector.match(/\s*[ >~+]\s*/g);
  118. return simpleSelectors.map((selector, selectorIndex) => {
  119. while (selector.match(REGEXP_PSEUDO_CLASSES)) {
  120. selector = selector.replace(REGEXP_PSEUDO_CLASSES, "").trim();
  121. }
  122. selector = selector.replace(/:?:[^(]+\(\)/g, "");
  123. if (selector == "") {
  124. selector = "*";
  125. }
  126. return selector + (separators && separators[selectorIndex] ? separators[selectorIndex] : "");
  127. }).join("");
  128. }).join(",");
  129. }
  130. return selector;
  131. }
  132. function getFontFamily(fontFamily) {
  133. fontFamily = fontFamily.toLowerCase().trim();
  134. if (fontFamily.match(/^'(.*)'$/)) {
  135. fontFamily = fontFamily.replace(/^'(.*)'$/, "$1");
  136. } else {
  137. fontFamily = fontFamily.replace(/^"(.*)"$/, "$1");
  138. }
  139. return fontFamily.trim();
  140. }
  141. })();