| var fs = require('fs'); |
| var path = require('path'); |
| var URL = require('url'); |
| |
| var toFileUrl = require('./jsdom/utils').toFileUrl; |
| var defineGetter = require('./jsdom/utils').defineGetter; |
| var defineSetter = require('./jsdom/utils').defineSetter; |
| var features = require('./jsdom/browser/documentfeatures'); |
| var dom = require('./jsdom/living'); |
| var browserAugmentation = require('./jsdom/browser/index').browserAugmentation; |
| var domToHtml = require('./jsdom/browser/domtohtml').domToHtml; |
| var VirtualConsole = require('./jsdom/virtual-console'); |
| |
| var request = function() { // lazy loading request |
| request = require('request'); |
| return request.apply(undefined, arguments); |
| } |
| |
| exports.getVirtualConsole = function (window) { |
| return window._virtualConsole; |
| }; |
| exports.debugMode = false; |
| |
| // Proxy feature functions to features module. |
| ['availableDocumentFeatures', |
| 'defaultDocumentFeatures', |
| 'applyDocumentFeatures'].forEach(function (propName) { |
| defineGetter(exports, propName, function () { |
| return features[propName]; |
| }); |
| defineSetter(exports, propName, function (val) { |
| return features[propName] = val; |
| }); |
| }); |
| |
| exports.jsdom = function (html, options) { |
| if (options === undefined) { |
| options = {}; |
| } |
| if (options.parsingMode === undefined || options.parsingMode === 'auto') { |
| options.parsingMode = 'html'; |
| } |
| |
| var browser = browserAugmentation(dom, options); |
| var doc = new browser.HTMLDocument(options); |
| |
| if (options.created) { |
| options.created(null, doc.parentWindow); |
| } |
| |
| features.applyDocumentFeatures(doc, options.features); |
| |
| if (html === undefined) { |
| html = ''; |
| } |
| html = String(html); |
| doc.write(html); |
| |
| if (doc.close && !options.deferClose) { |
| doc.close(); |
| } |
| |
| return doc; |
| }; |
| |
| exports.jQueryify = exports.jsdom.jQueryify = function (window, jqueryUrl, callback) { |
| if (!window || !window.document) { |
| return; |
| } |
| |
| var features = window.document.implementation._features; |
| window.document.implementation._addFeature('FetchExternalResources', ['script']); |
| window.document.implementation._addFeature('ProcessExternalResources', ['script']); |
| window.document.implementation._addFeature('MutationEvents', ['2.0']); |
| |
| var scriptEl = window.document.createElement('script'); |
| scriptEl.className = 'jsdom'; |
| scriptEl.src = jqueryUrl; |
| scriptEl.onload = scriptEl.onerror = function () { |
| window.document.implementation._features = features; |
| |
| if (callback) { |
| callback(window, window.jQuery); |
| } |
| }; |
| |
| window.document.body.appendChild(scriptEl); |
| }; |
| |
| exports.env = exports.jsdom.env = function () { |
| var config = getConfigFromArguments(arguments); |
| |
| if (config.file) { |
| fs.readFile(config.file, 'utf-8', function (err, text) { |
| if (err) { |
| if (config.created) { |
| config.created(err); |
| } |
| if (config.done) { |
| config.done([err]); |
| } |
| return; |
| } |
| |
| setParsingModeFromExtension(config, config.file); |
| |
| config.html = text; |
| processHTML(config); |
| }); |
| } else if (config.html !== undefined) { |
| processHTML(config); |
| } else if (config.url) { |
| handleUrl(config); |
| } else if (config.somethingToAutodetect !== undefined) { |
| var url = URL.parse(config.somethingToAutodetect); |
| if (url.protocol && url.hostname) { |
| config.url = config.somethingToAutodetect; |
| handleUrl(config.somethingToAutodetect); |
| } else { |
| fs.readFile(config.somethingToAutodetect, 'utf-8', function (err, text) { |
| if (err) { |
| // the toString() test is because in Node.js, there is no proper code for this. |
| // This is fixed in io.js: https://github.com/iojs/io.js/issues/517 so: |
| // TODO: remove when we start requiring io.js |
| if (err.code === 'ENOENT' || err.code === 'ENAMETOOLONG' |
| || (err.toString() == 'Error: Path must be a string without null bytes.') |
| ) { |
| config.html = config.somethingToAutodetect; |
| processHTML(config); |
| } else { |
| if (config.created) { |
| config.created(err); |
| } |
| if (config.done) { |
| config.done([err]); |
| } |
| } |
| } else { |
| setParsingModeFromExtension(config, config.somethingToAutodetect); |
| |
| config.html = text; |
| config.url = toFileUrl(config.somethingToAutodetect); |
| processHTML(config); |
| } |
| }); |
| } |
| } |
| |
| function handleUrl() { |
| var options = { |
| uri: config.url, |
| encoding: config.encoding || 'utf8', |
| headers: config.headers || {}, |
| proxy: config.proxy || null, |
| jar: config.jar !== undefined ? config.jar : true |
| }; |
| |
| request(options, function (err, res, responseText) { |
| if (err) { |
| if (config.created) { |
| config.created(err); |
| } |
| if (config.done) { |
| config.done([err]); |
| } |
| return; |
| } |
| |
| // The use of `res.request.uri.href` ensures that `window.location.href` |
| // is updated when `request` follows redirects. |
| config.html = responseText; |
| config.url = res.request.uri.href; |
| |
| if (config.parsingMode === "auto" && ( |
| res.headers["content-type"] === "application/xml" || |
| res.headers["content-type"] === "text/xml" || |
| res.headers["content-type"] === "application/xhtml+xml")) { |
| config.parsingMode = "xml"; |
| } |
| |
| processHTML(config); |
| }); |
| } |
| }; |
| |
| exports.serializeDocument = function (doc) { |
| return domToHtml(doc, true); |
| }; |
| |
| function processHTML(config) { |
| var options = { |
| features: config.features, |
| url: config.url, |
| parser: config.parser, |
| parsingMode: config.parsingMode, |
| created: config.created, |
| resourceLoader: config.resourceLoader |
| }; |
| |
| if (config.document) { |
| options.referrer = config.document.referrer; |
| options.cookie = config.document.cookie; |
| options.cookieDomain = config.document.cookieDomain; |
| } |
| |
| var window = exports.jsdom(config.html, options).parentWindow; |
| var features = JSON.parse(JSON.stringify(window.document.implementation._features)); |
| |
| var docsLoaded = 0; |
| var totalDocs = config.scripts.length + config.src.length; |
| var readyState = null; |
| var errors = []; |
| |
| if (!window || !window.document) { |
| if (config.created) { |
| config.created(new Error('JSDOM: a window object could not be created.')); |
| } |
| if (config.done) { |
| config.done([new Error('JSDOM: a window object could not be created.')]); |
| } |
| return; |
| } |
| |
| window.document.implementation._addFeature('FetchExternalResources', ['script']); |
| window.document.implementation._addFeature('ProcessExternalResources', ['script']); |
| window.document.implementation._addFeature('MutationEvents', ['2.0']); |
| |
| function scriptComplete() { |
| docsLoaded++; |
| |
| if (docsLoaded >= totalDocs) { |
| window.document.implementation._features = features; |
| |
| errors = errors.concat(window.document.errors || []); |
| if (errors.length === 0) { |
| errors = null; |
| } |
| |
| process.nextTick(function() { |
| if (config.loaded) { |
| config.loaded(errors, window); |
| } |
| if (config.done) { |
| config.done(errors, window); |
| } |
| }); |
| } |
| } |
| |
| function handleScriptError(e) { |
| if (!errors) { |
| errors = []; |
| } |
| errors.push(e.error || e.message); |
| |
| // nextTick so that an exception within scriptComplete won't cause |
| // another script onerror (which would be an infinite loop) |
| process.nextTick(scriptComplete); |
| } |
| |
| if (config.scripts.length > 0 || config.src.length > 0) { |
| config.scripts.forEach(function (scriptSrc) { |
| var script = window.document.createElement('script'); |
| script.className = 'jsdom'; |
| script.onload = scriptComplete; |
| script.onerror = handleScriptError; |
| script.src = scriptSrc; |
| |
| try { |
| // protect against invalid dom |
| // ex: http://www.google.com/foo#bar |
| window.document.documentElement.appendChild(script); |
| } catch (e) { |
| handleScriptError(e); |
| } |
| }); |
| |
| config.src.forEach(function (scriptText) { |
| var script = window.document.createElement('script'); |
| script.onload = scriptComplete; |
| script.onerror = handleScriptError; |
| script.text = scriptText; |
| |
| window.document.documentElement.appendChild(script); |
| window.document.documentElement.removeChild(script); |
| }); |
| } else { |
| if (window.document.readyState === 'complete') { |
| scriptComplete(); |
| } else { |
| window.addEventListener('load', function() { |
| scriptComplete(); |
| }); |
| } |
| } |
| } |
| |
| function getConfigFromArguments(args, callback) { |
| var config = {}; |
| if (typeof args[0] === 'object') { |
| var configToClone = args[0]; |
| Object.keys(configToClone).forEach(function (key) { |
| config[key] = configToClone[key]; |
| }); |
| } else { |
| var stringToAutodetect = null; |
| |
| Array.prototype.forEach.call(args, function (arg) { |
| switch (typeof arg) { |
| case 'string': |
| config.somethingToAutodetect = arg; |
| break; |
| case 'function': |
| config.done = arg; |
| break; |
| case 'object': |
| if (Array.isArray(arg)) { |
| config.scripts = arg; |
| } else { |
| extend(config, arg); |
| } |
| break; |
| } |
| }); |
| } |
| |
| if (!config.done && !config.created && !config.loaded) { |
| throw new Error('Must pass a "created", "loaded", "done" option or a callback to jsdom.env.'); |
| } |
| |
| if (config.somethingToAutodetect === undefined && |
| config.html === undefined && !config.file && !config.url) { |
| throw new Error('Must pass a "html", "file", or "url" option, or a string, to jsdom.env'); |
| } |
| |
| config.scripts = ensureArray(config.scripts); |
| config.src = ensureArray(config.src); |
| config.parsingMode = config.parsingMode || "auto"; |
| |
| config.features = config.features || { |
| FetchExternalResources: false, |
| ProcessExternalResources: false, |
| SkipExternalResources: false |
| }; |
| |
| if (!config.url && config.file) { |
| config.url = toFileUrl(config.file); |
| } |
| |
| return config; |
| } |
| |
| function ensureArray(value) { |
| var array = value || []; |
| if (typeof array === 'string') { |
| array = [array]; |
| } |
| return array; |
| } |
| |
| function extend(config, overrides) { |
| Object.keys(overrides).forEach(function (key) { |
| config[key] = overrides[key]; |
| }); |
| } |
| |
| function setParsingModeFromExtension(config, filename) { |
| if (config.parsingMode === "auto") { |
| var ext = path.extname(filename); |
| if (ext === ".xhtml" || ext === ".xml") { |
| config.parsingMode = "xml"; |
| } |
| } |
| } |