single-file-node.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. /* global require, exports, Buffer */
  2. const fs = require("fs");
  3. const crypto = require("crypto");
  4. const jsdom = require("jsdom");
  5. const dataUri = require("strong-data-uri");
  6. const iconv = require("iconv-lite");
  7. const request = require("request-promise-native");
  8. const { JSDOM } = jsdom;
  9. const ONE_MB = 1024 * 1024;
  10. const PREFIX_CONTENT_TYPE_TEXT = "text/";
  11. const SCRIPTS = [
  12. "./lib/single-file/util/doc-util-core.js",
  13. "./lib/single-file/single-file-core.js",
  14. "./lib/single-file/vendor/css-tree.js",
  15. "./lib/single-file/util/doc-helper.js",
  16. "./lib/single-file/vendor/html-srcset-parser.js",
  17. "./lib/single-file/vendor/css-minifier.js",
  18. "./lib/single-file/modules/html-minifier.js",
  19. "./lib/single-file/modules/css-fonts-minifier.js",
  20. "./lib/single-file/modules/css-fonts-alt-minifier.js",
  21. "./lib/single-file/modules/css-matched-rules.js",
  22. "./lib/single-file/modules/css-medias-alt-minifier.js",
  23. "./lib/single-file/modules/css-rules-minifier.js",
  24. "./lib/single-file/modules/html-images-alt-minifier.js",
  25. "./lib/single-file/modules/html-serializer.js",
  26. "./lib/single-file/vendor/css-font-property-parser.js",
  27. "./lib/single-file/vendor/css-media-query-parser.js"
  28. ];
  29. SCRIPTS.forEach(scriptPath => eval(fs.readFileSync(scriptPath).toString()));
  30. const modules = {
  31. docHelper: this.docHelper,
  32. srcsetParser: this.srcsetParser,
  33. cssMinifier: this.cssMinifier,
  34. htmlMinifier: this.htmlMinifier,
  35. fontsMinifier: this.fontsMinifier,
  36. fontsAltMinifier: this.fontsAltMinifier,
  37. cssRulesMinifier: this.cssRulesMinifier,
  38. matchedRules: this.matchedRules,
  39. mediasMinifier: this.mediasMinifier,
  40. imagesAltMinifier: this.imagesAltMinifier,
  41. serializer: this.serializer
  42. };
  43. modules.fontsAltMinifier.cssTree = this.cssTree;
  44. modules.fontsMinifier.cssTree = this.cssTree;
  45. modules.fontsMinifier.fontPropertyParser = this.fontPropertyParser;
  46. modules.fontsMinifier.docHelper = this.docHelper;
  47. modules.matchedRules.cssTree = this.cssTree;
  48. modules.mediasMinifier.cssTree = this.cssTree;
  49. modules.mediasMinifier.mediaQueryParser = this.mediaQueryParser;
  50. modules.cssRulesMinifier.cssTree = this.cssTree;
  51. modules.imagesAltMinifier.srcsetParser = this.srcsetParser;
  52. const domUtil = {
  53. getContent, parseDocContent, parseSVGContent, isValidFontUrl, getContentSize, digestText
  54. };
  55. exports.getClass = () => {
  56. const DocUtil = this.DocUtilCore.getClass(modules, domUtil);
  57. return this.SingleFileCore.getClass(DocUtil, this.cssTree);
  58. };
  59. function parseDocContent(content, baseURI) {
  60. const doc = (new JSDOM(content, {
  61. contentType: "text/html"
  62. })).window.document;
  63. let baseElement = doc.querySelector("base");
  64. if (!baseElement || !baseElement.getAttribute("href")) {
  65. if (baseElement) {
  66. baseElement.remove();
  67. }
  68. baseElement = doc.createElement("base");
  69. baseElement.setAttribute("href", baseURI);
  70. doc.head.insertBefore(baseElement, doc.head.firstChild);
  71. }
  72. return doc;
  73. }
  74. function parseSVGContent(content) {
  75. return (new JSDOM(content, {
  76. contentType: "image/svg+xml"
  77. })).window.document;
  78. }
  79. async function digestText(algo, text) {
  80. const hash = crypto.createHash(algo.replace("-", "").toLowerCase());
  81. hash.update(text, "utf-8");
  82. return hash.digest("hex");
  83. }
  84. function getContentSize(content) {
  85. return Buffer.byteLength(content, "utf-8");
  86. }
  87. function isValidFontUrl(/* urlFunction */) {
  88. // TODO?
  89. return true;
  90. }
  91. async function getContent(resourceURL, options) {
  92. const requestOptions = {
  93. method: "GET",
  94. uri: resourceURL,
  95. resolveWithFullResponse: true,
  96. encoding: null,
  97. headers: {
  98. "User-Agent": options.userAgent
  99. }
  100. };
  101. let resourceContent;
  102. try {
  103. resourceContent = await request(requestOptions);
  104. } catch (e) {
  105. return options.asDataURI ? "data:base64," : "";
  106. }
  107. let contentType = resourceContent.headers["content-type"];
  108. let charset;
  109. if (contentType) {
  110. const matchContentType = contentType.toLowerCase().split(";");
  111. contentType = matchContentType[0].trim();
  112. if (!contentType.includes("/")) {
  113. contentType = null;
  114. }
  115. const charsetValue = matchContentType[1] && matchContentType[1].trim();
  116. if (charsetValue) {
  117. const matchCharset = charsetValue.match(/^charset=(.*)/);
  118. if (matchCharset && matchCharset[1]) {
  119. charset = this.docHelper.removeQuotes(matchCharset[1].trim());
  120. }
  121. }
  122. }
  123. if (!charset && options.charset) {
  124. charset = options.charset;
  125. }
  126. if (options && options.asDataURI) {
  127. try {
  128. const buffer = resourceContent.body;
  129. if (options.maxResourceSizeEnabled && buffer.byteLength > options.maxResourceSize * ONE_MB) {
  130. return { data: "data:base64,", resourceURL };
  131. } else {
  132. return { data: dataUri.encode(buffer, contentType), resourceURL };
  133. }
  134. } catch (e) {
  135. return { data: "data:base64,", resourceURL };
  136. }
  137. } else {
  138. if (resourceContent.statusCode >= 400 || (options.validateTextContentType && contentType && !contentType.startsWith(PREFIX_CONTENT_TYPE_TEXT))) {
  139. return { data: "", resourceURL };
  140. }
  141. if (!charset) {
  142. charset = "utf-8";
  143. }
  144. try {
  145. return { data: iconv.decode(resourceContent.body, charset), charset };
  146. } catch (e) {
  147. return { data: resourceContent.body.toString("utf8"), charset: "utf8" };
  148. }
  149. }
  150. }