single-file-node.js 4.9 KB

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