single-file-node.js 4.8 KB

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