Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

434 строки
13 KiB

  1. "use strict";
  2. /* eslint-disable no-unused-expressions */
  3. () => `jsdom 7.x onward only works on Node.js 4 or newer: https://github.com/tmpvar/jsdom#install`;
  4. /* eslint-enable no-unused-expressions */
  5. const fs = require("fs");
  6. const path = require("path");
  7. const CookieJar = require("tough-cookie").CookieJar;
  8. const toFileUrl = require("./jsdom/utils").toFileUrl;
  9. const defineGetter = require("./jsdom/utils").defineGetter;
  10. const defineSetter = require("./jsdom/utils").defineSetter;
  11. const parseContentType = require("./jsdom/living/helpers/headers").parseContentType;
  12. const documentFeatures = require("./jsdom/browser/documentfeatures");
  13. const domToHtml = require("./jsdom/browser/domtohtml").domToHtml;
  14. const Window = require("./jsdom/browser/Window");
  15. const resourceLoader = require("./jsdom/browser/resource-loader");
  16. const VirtualConsole = require("./jsdom/virtual-console");
  17. const locationInfo = require("./jsdom/living/helpers/internal-constants").locationInfo;
  18. const idlUtils = require("./jsdom/living/generated/utils");
  19. const blobSymbols = require("./jsdom/living/blob-symbols");
  20. require("./jsdom/living"); // Enable living standard features
  21. /* eslint-disable no-restricted-modules */
  22. // TODO: stop using the built-in URL in favor of the spec-compliant whatwg-url package
  23. // This legacy usage is in the process of being purged.
  24. const URL = require("url");
  25. /* eslint-enable no-restricted-modules */
  26. const canReadFilesFromFS = Boolean(fs.readFile); // in a browserify environment, this isn't present
  27. exports.createVirtualConsole = function (options) {
  28. return new VirtualConsole(options);
  29. };
  30. exports.getVirtualConsole = function (window) {
  31. return window._virtualConsole;
  32. };
  33. exports.createCookieJar = function () {
  34. return new CookieJar(null, { looseMode: true });
  35. };
  36. exports.nodeLocation = function (node) {
  37. return idlUtils.implForWrapper(node)[locationInfo];
  38. };
  39. exports.reconfigureWindow = function (window, newProps) {
  40. if ("top" in newProps) {
  41. window._top = newProps.top;
  42. }
  43. };
  44. exports.debugMode = false;
  45. // Proxy feature functions to features module.
  46. for (const propName of ["availableDocumentFeatures", "defaultDocumentFeatures", "applyDocumentFeatures"]) {
  47. defineGetter(exports, propName, () => documentFeatures[propName]);
  48. defineSetter(exports, propName, val => {
  49. documentFeatures[propName] = val;
  50. });
  51. }
  52. exports.jsdom = function (html, options) {
  53. if (options === undefined) {
  54. options = {};
  55. }
  56. if (options.parsingMode === undefined || options.parsingMode === "auto") {
  57. options.parsingMode = "html";
  58. }
  59. if (options.parsingMode !== "html" && options.parsingMode !== "xml") {
  60. throw new RangeError(`Invalid parsingMode option ${JSON.stringify(options.parsingMode)}; must be either "html", ` +
  61. `"xml", "auto", or undefined`);
  62. }
  63. options.encoding = options.encoding || "UTF-8";
  64. setGlobalDefaultConfig(options);
  65. // Back-compat hack: we have previously suggested nesting these under document, for jsdom.env at least.
  66. // So we need to support that.
  67. if (options.document) {
  68. if (options.document.cookie !== undefined) {
  69. options.cookie = options.document.cookie;
  70. }
  71. if (options.document.referrer !== undefined) {
  72. options.referrer = options.document.referrer;
  73. }
  74. }
  75. // List options explicitly to be clear which are passed through
  76. const window = new Window({
  77. parsingMode: options.parsingMode,
  78. contentType: options.contentType,
  79. encoding: options.encoding,
  80. parser: options.parser,
  81. url: options.url,
  82. lastModified: options.lastModified,
  83. referrer: options.referrer,
  84. cookieJar: options.cookieJar,
  85. cookie: options.cookie,
  86. resourceLoader: options.resourceLoader,
  87. deferClose: options.deferClose,
  88. concurrentNodeIterators: options.concurrentNodeIterators,
  89. virtualConsole: options.virtualConsole,
  90. pool: options.pool,
  91. agentOptions: options.agentOptions,
  92. strictSSL: options.strictSSL,
  93. userAgent: options.userAgent
  94. });
  95. documentFeatures.applyDocumentFeatures(window.document, options.features);
  96. if (options.created) {
  97. options.created(null, window.document.defaultView);
  98. }
  99. if (options.parsingMode === "html") {
  100. if (html === undefined || html === "") {
  101. html = "<html><head></head><body></body></html>";
  102. }
  103. window.document.write(html);
  104. } else if (options.parsingMode === "xml") {
  105. if (html !== undefined) {
  106. const documentImpl = idlUtils.implForWrapper(window.document);
  107. documentImpl._htmlToDom.appendHtmlToDocument(html, documentImpl);
  108. }
  109. }
  110. if (window.document.close && !options.deferClose) {
  111. window.document.close();
  112. }
  113. return window.document;
  114. };
  115. exports.jQueryify = exports.jsdom.jQueryify = function (window, jqueryUrl, callback) {
  116. if (!window || !window.document) {
  117. return;
  118. }
  119. const implImpl = idlUtils.implForWrapper(window.document.implementation);
  120. const features = implImpl._features;
  121. implImpl._addFeature("FetchExternalResources", ["script"]);
  122. implImpl._addFeature("ProcessExternalResources", ["script"]);
  123. implImpl._addFeature("MutationEvents", ["2.0"]);
  124. const scriptEl = window.document.createElement("script");
  125. scriptEl.className = "jsdom";
  126. scriptEl.src = jqueryUrl;
  127. scriptEl.onload = scriptEl.onerror = () => {
  128. implImpl._features = features;
  129. if (callback) {
  130. callback(window, window.jQuery);
  131. }
  132. };
  133. window.document.body.appendChild(scriptEl);
  134. };
  135. exports.env = exports.jsdom.env = function () {
  136. const config = getConfigFromArguments(arguments);
  137. let req = null;
  138. if (config.file && canReadFilesFromFS) {
  139. req = resourceLoader.readFile(config.file,
  140. { defaultEncoding: config.defaultEncoding, detectMetaCharset: true },
  141. (err, text, res) => {
  142. if (err) {
  143. reportInitError(err, config);
  144. return;
  145. }
  146. const contentType = parseContentType(res.headers["content-type"]);
  147. config.encoding = contentType.get("charset");
  148. setParsingModeFromExtension(config, config.file);
  149. config.html = text;
  150. processHTML(config);
  151. });
  152. } else if (config.html !== undefined) {
  153. processHTML(config);
  154. } else if (config.url) {
  155. req = handleUrl(config);
  156. } else if (config.somethingToAutodetect !== undefined) {
  157. const url = URL.parse(config.somethingToAutodetect);
  158. if (url.protocol && url.hostname) {
  159. config.url = config.somethingToAutodetect;
  160. req = handleUrl(config.somethingToAutodetect);
  161. } else if (canReadFilesFromFS) {
  162. req = resourceLoader.readFile(config.somethingToAutodetect,
  163. { defaultEncoding: config.defaultEncoding, detectMetaCharset: true },
  164. (err, text, res) => {
  165. if (err) {
  166. if (err.code === "ENOENT" || err.code === "ENAMETOOLONG") {
  167. config.html = config.somethingToAutodetect;
  168. processHTML(config);
  169. } else {
  170. reportInitError(err, config);
  171. }
  172. } else {
  173. const contentType = parseContentType(res.headers["content-type"]);
  174. config.encoding = contentType.get("charset");
  175. setParsingModeFromExtension(config, config.somethingToAutodetect);
  176. config.html = text;
  177. config.url = toFileUrl(config.somethingToAutodetect);
  178. processHTML(config);
  179. }
  180. });
  181. } else {
  182. config.html = config.somethingToAutodetect;
  183. processHTML(config);
  184. }
  185. }
  186. function handleUrl() {
  187. config.cookieJar = config.cookieJar || exports.createCookieJar();
  188. const options = {
  189. defaultEncoding: config.defaultEncoding,
  190. detectMetaCharset: true,
  191. headers: config.headers,
  192. pool: config.pool,
  193. agentOptions: config.agentOptions,
  194. strictSSL: config.strictSSL,
  195. proxy: config.proxy,
  196. cookieJar: config.cookieJar,
  197. userAgent: config.userAgent
  198. };
  199. return resourceLoader.download(config.url, options, (err, responseText, res) => {
  200. if (err) {
  201. reportInitError(err, config);
  202. return;
  203. }
  204. // The use of `res.request.uri.href` ensures that `window.location.href`
  205. // is updated when `request` follows redirects.
  206. config.html = responseText;
  207. config.url = res.request.uri.href;
  208. if (res.headers["last-modified"]) {
  209. config.lastModified = new Date(res.headers["last-modified"]);
  210. }
  211. const contentType = parseContentType(res.headers["content-type"]);
  212. if (config.parsingMode === "auto") {
  213. if (contentType.isXML()) {
  214. config.parsingMode = "xml";
  215. }
  216. }
  217. config.encoding = contentType.get("charset");
  218. processHTML(config);
  219. });
  220. }
  221. return req;
  222. };
  223. exports.serializeDocument = function (doc) {
  224. return domToHtml([doc]);
  225. };
  226. exports.blobToBuffer = function (blob) {
  227. return blob && blob[blobSymbols.buffer];
  228. };
  229. function processHTML(config) {
  230. const window = exports.jsdom(config.html, config).defaultView;
  231. const implImpl = idlUtils.implForWrapper(window.document.implementation);
  232. const features = JSON.parse(JSON.stringify(implImpl._features));
  233. let docsLoaded = 0;
  234. const totalDocs = config.scripts.length + config.src.length;
  235. if (!window || !window.document) {
  236. reportInitError(new Error("JSDOM: a window object could not be created."), config);
  237. return;
  238. }
  239. function scriptComplete() {
  240. docsLoaded++;
  241. if (docsLoaded >= totalDocs) {
  242. implImpl._features = features;
  243. process.nextTick(() => {
  244. if (config.onload) {
  245. config.onload(window);
  246. }
  247. if (config.done) {
  248. config.done(null, window);
  249. }
  250. });
  251. }
  252. }
  253. function handleScriptError() {
  254. // nextTick so that an exception within scriptComplete won't cause
  255. // another script onerror (which would be an infinite loop)
  256. process.nextTick(scriptComplete);
  257. }
  258. if (config.scripts.length > 0 || config.src.length > 0) {
  259. implImpl._addFeature("FetchExternalResources", ["script"]);
  260. implImpl._addFeature("ProcessExternalResources", ["script"]);
  261. implImpl._addFeature("MutationEvents", ["2.0"]);
  262. for (const scriptSrc of config.scripts) {
  263. const script = window.document.createElement("script");
  264. script.className = "jsdom";
  265. script.onload = scriptComplete;
  266. script.onerror = handleScriptError;
  267. script.src = scriptSrc;
  268. window.document.body.appendChild(script);
  269. }
  270. for (const scriptText of config.src) {
  271. const script = window.document.createElement("script");
  272. script.onload = scriptComplete;
  273. script.onerror = handleScriptError;
  274. script.text = scriptText;
  275. window.document.documentElement.appendChild(script);
  276. window.document.documentElement.removeChild(script);
  277. }
  278. } else if (window.document.readyState === "complete") {
  279. scriptComplete();
  280. } else {
  281. window.addEventListener("load", scriptComplete);
  282. }
  283. }
  284. function setGlobalDefaultConfig(config) {
  285. config.pool = config.pool !== undefined ? config.pool : {
  286. maxSockets: 6
  287. };
  288. config.agentOptions = config.agentOptions !== undefined ? config.agentOptions : {
  289. keepAlive: true,
  290. keepAliveMsecs: 115 * 1000
  291. };
  292. config.strictSSL = config.strictSSL !== undefined ? config.strictSSL : true;
  293. config.userAgent = config.userAgent || "Node.js (" + process.platform + "; U; rv:" + process.version + ")";
  294. }
  295. function getConfigFromArguments(args) {
  296. const config = {};
  297. if (typeof args[0] === "object") {
  298. Object.assign(config, args[0]);
  299. } else {
  300. for (const arg of args) {
  301. switch (typeof arg) {
  302. case "string":
  303. config.somethingToAutodetect = arg;
  304. break;
  305. case "function":
  306. config.done = arg;
  307. break;
  308. case "object":
  309. if (Array.isArray(arg)) {
  310. config.scripts = arg;
  311. } else {
  312. Object.assign(config, arg);
  313. }
  314. break;
  315. }
  316. }
  317. }
  318. if (!config.done && !config.created && !config.onload) {
  319. throw new Error("Must pass a \"created\", \"onload\", or \"done\" option, or a callback, to jsdom.env");
  320. }
  321. if (config.somethingToAutodetect === undefined &&
  322. config.html === undefined && !config.file && !config.url) {
  323. throw new Error("Must pass a \"html\", \"file\", or \"url\" option, or a string, to jsdom.env");
  324. }
  325. config.scripts = ensureArray(config.scripts);
  326. config.src = ensureArray(config.src);
  327. config.parsingMode = config.parsingMode || "auto";
  328. config.features = config.features || {
  329. FetchExternalResources: false,
  330. ProcessExternalResources: false,
  331. SkipExternalResources: false
  332. };
  333. if (!config.url && config.file) {
  334. config.url = toFileUrl(config.file);
  335. }
  336. config.defaultEncoding = config.defaultEncoding || "windows-1252";
  337. setGlobalDefaultConfig(config);
  338. return config;
  339. }
  340. function reportInitError(err, config) {
  341. if (config.created) {
  342. config.created(err);
  343. }
  344. if (config.done) {
  345. config.done(err);
  346. }
  347. }
  348. function ensureArray(value) {
  349. let array = value || [];
  350. if (typeof array === "string") {
  351. array = [array];
  352. }
  353. return array;
  354. }
  355. function setParsingModeFromExtension(config, filename) {
  356. if (config.parsingMode === "auto") {
  357. const ext = path.extname(filename);
  358. if (ext === ".xhtml" || ext === ".xml") {
  359. config.parsingMode = "xml";
  360. }
  361. }
  362. }