jsdom.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. var dom = exports.dom = require("./jsdom/level3/index").dom,
  2. fs = require("fs"),
  3. pkg = JSON.parse(fs.readFileSync(__dirname + "/../package.json")),
  4. request = require('request'),
  5. URL = require('url');
  6. var style = require('./jsdom/level2/style');
  7. exports.defaultLevel = dom.level3.html;
  8. exports.browserAugmentation = require("./jsdom/browser/index").browserAugmentation;
  9. exports.windowAugmentation = require("./jsdom/browser/index").windowAugmentation;
  10. exports.debugMode = false;
  11. var createWindow = exports.createWindow = require("./jsdom/browser/index").createWindow;
  12. exports.__defineGetter__('version', function() {
  13. return pkg.version;
  14. });
  15. exports.level = function (level, feature) {
  16. if(!feature) feature = 'core'
  17. return require('./jsdom/level' + level + '/' + feature).dom['level' + level][feature]
  18. }
  19. exports.jsdom = function (html, level, options) {
  20. options = options || {};
  21. if(typeof level == "string") {
  22. level = exports.level(level, 'html')
  23. } else {
  24. level = level || exports.defaultLevel;
  25. }
  26. if (!options.url) {
  27. options.url = (module.parent.id === 'jsdom') ?
  28. module.parent.parent.filename :
  29. module.parent.filename;
  30. }
  31. if (options.features && options.features.QuerySelector) {
  32. require("./jsdom/selectors/index").applyQuerySelectorPrototype(level);
  33. }
  34. var browser = exports.browserAugmentation(level, options),
  35. doc = (browser.HTMLDocument) ?
  36. new browser.HTMLDocument(options) :
  37. new browser.Document(options);
  38. exports.applyDocumentFeatures(doc, options.features);
  39. if (!!html) {
  40. doc.write(html + '');
  41. } else {
  42. doc.write('<html><head></head><body></body></html>');
  43. }
  44. if (doc.close && !options.deferClose) {
  45. doc.close();
  46. }
  47. // Kept for backwards-compatibility. The window is lazily created when
  48. // document.parentWindow or document.defaultView is accessed.
  49. doc.createWindow = function() {
  50. // Remove ourself
  51. if (doc.createWindow) {
  52. delete doc.createWindow;
  53. }
  54. return doc.parentWindow;
  55. };
  56. return doc;
  57. };
  58. exports.html = function(html, level, options) {
  59. html += '';
  60. // TODO: cache a regex and use it here instead
  61. // or make the parser handle it
  62. var htmlLowered = html.toLowerCase();
  63. // body
  64. if (!~htmlLowered.indexOf('<body')) {
  65. html = '<body>' + html + '</body>';
  66. }
  67. // html
  68. if (!~htmlLowered.indexOf('<html')) {
  69. html = '<html>' + html + '</html>';
  70. }
  71. return exports.jsdom(html, level, options);
  72. };
  73. exports.availableDocumentFeatures = [
  74. 'FetchExternalResources',
  75. 'ProcessExternalResources',
  76. 'MutationEvents',
  77. 'QuerySelector'
  78. ];
  79. exports.defaultDocumentFeatures = {
  80. "FetchExternalResources" : ['script'/*, 'img', 'css', 'frame', 'link'*/],
  81. "ProcessExternalResources" : ['script'/*, 'frame', 'iframe'*/],
  82. "MutationEvents" : '2.0',
  83. "QuerySelector" : false
  84. };
  85. exports.applyDocumentFeatures = function(doc, features) {
  86. var i, maxFeatures = exports.availableDocumentFeatures.length,
  87. defaultFeatures = exports.defaultDocumentFeatures,
  88. j,
  89. k,
  90. featureName,
  91. featureSource;
  92. features = features || {};
  93. for (i=0; i<maxFeatures; i++) {
  94. featureName = exports.availableDocumentFeatures[i];
  95. if (typeof features[featureName] !== 'undefined') {
  96. featureSource = features[featureName];
  97. } else if (defaultFeatures[featureName]) {
  98. featureSource = defaultFeatures[featureName];
  99. } else {
  100. continue;
  101. }
  102. doc.implementation.removeFeature(featureName);
  103. if (typeof featureSource !== 'undefined') {
  104. if (featureSource instanceof Array) {
  105. k = featureSource.length;
  106. for (j=0; j<k; j++) {
  107. doc.implementation.addFeature(featureName, featureSource[j]);
  108. }
  109. } else {
  110. doc.implementation.addFeature(featureName, featureSource);
  111. }
  112. }
  113. }
  114. };
  115. exports.jQueryify = exports.jsdom.jQueryify = function (window /* path [optional], callback */) {
  116. if (!window || !window.document) { return; }
  117. var args = Array.prototype.slice.call(arguments),
  118. callback = (typeof(args[args.length - 1]) === 'function') && args.pop(),
  119. path,
  120. jQueryTag = window.document.createElement("script");
  121. if (args.length > 1 && typeof(args[1] === 'string')) {
  122. path = args[1];
  123. }
  124. var features = window.document.implementation._features;
  125. window.document.implementation.addFeature('FetchExternalResources', ['script']);
  126. window.document.implementation.addFeature('ProcessExternalResources', ['script']);
  127. window.document.implementation.addFeature('MutationEvents', ["1.0"]);
  128. jQueryTag.src = path || 'http://code.jquery.com/jquery-latest.js';
  129. window.document.body.appendChild(jQueryTag);
  130. jQueryTag.onload = function() {
  131. if (callback) {
  132. callback(window, window.jQuery);
  133. }
  134. window.document.implementation._features = features;
  135. };
  136. return window;
  137. };
  138. exports.env = exports.jsdom.env = function() {
  139. var
  140. args = Array.prototype.slice.call(arguments),
  141. config = exports.env.processArguments(args),
  142. callback = config.done,
  143. processHTML = function(err, html) {
  144. html += '';
  145. if(err) {
  146. return callback(err);
  147. }
  148. config.scripts = config.scripts || [];
  149. if (typeof config.scripts === 'string') {
  150. config.scripts = [config.scripts];
  151. }
  152. config.src = config.src || [];
  153. if (typeof config.src === 'string') {
  154. config.src = [config.src];
  155. }
  156. var
  157. options = {
  158. features: {
  159. 'FetchExternalResources' : false,
  160. 'ProcessExternalResources' : false
  161. },
  162. url: config.url
  163. },
  164. window = exports.html(html, null, options).createWindow(),
  165. features = window.document.implementation._features,
  166. docsLoaded = 0,
  167. totalDocs = config.scripts.length,
  168. readyState = null,
  169. errors = null;
  170. if (!window || !window.document) {
  171. return callback(new Error('JSDOM: a window object could not be created.'));
  172. }
  173. window.document.implementation.addFeature('FetchExternalResources', ['script']);
  174. window.document.implementation.addFeature('ProcessExternalResources', ['script']);
  175. window.document.implementation.addFeature('MutationEvents', ['1.0']);
  176. var scriptComplete = function() {
  177. docsLoaded++;
  178. if (docsLoaded >= totalDocs) {
  179. window.document.implementation._features = features;
  180. callback(errors, window);
  181. }
  182. }
  183. if (config.scripts.length > 0 || config.src.length > 0) {
  184. config.scripts.forEach(function(src) {
  185. var script = window.document.createElement('script');
  186. script.onload = function() {
  187. scriptComplete()
  188. };
  189. script.onerror = function(e) {
  190. if (!errors) {
  191. errors = [];
  192. }
  193. errors.push(e.error);
  194. scriptComplete();
  195. };
  196. script.src = src;
  197. window.document.documentElement.appendChild(script);
  198. });
  199. config.src.forEach(function(src) {
  200. var script = window.document.createElement('script');
  201. script.onload = function() {
  202. process.nextTick(scriptComplete);
  203. };
  204. script.onerror = function(e) {
  205. if (!errors) {
  206. errors = [];
  207. }
  208. errors.push(e.error || e.message);
  209. // nextTick so that an exception within scriptComplete won't cause
  210. // another script onerror (which would be an infinite loop)
  211. process.nextTick(scriptComplete);
  212. };
  213. script.text = src;
  214. window.document.documentElement.appendChild(script);
  215. window.document.documentElement.removeChild(script);
  216. });
  217. } else {
  218. callback(errors, window);
  219. }
  220. };
  221. config.html += '';
  222. // Handle markup
  223. if (config.html.indexOf("\n") > 0 || config.html.match(/^\W*</)) {
  224. processHTML(null, config.html);
  225. // Handle url/file
  226. } else {
  227. var url = URL.parse(config.html);
  228. config.url = config.url || url.href;
  229. if (url.hostname) {
  230. request({ uri: url,
  231. encoding: config.encoding || 'utf8',
  232. headers: config.headers || {}
  233. },
  234. function(err, request, body) {
  235. processHTML(err, body);
  236. });
  237. } else {
  238. fs.readFile(url.pathname, processHTML);
  239. }
  240. }
  241. };
  242. /*
  243. Since jsdom.env() is a helper for quickly and easily setting up a
  244. window with scripts and such already loaded into it, the arguments
  245. should be fairly flexible. Here are the requirements
  246. 1) collect `html` (url, string, or file on disk) (STRING)
  247. 2) load `code` into the window (array of scripts) (ARRAY)
  248. 3) callback when resources are `done` (FUNCTION)
  249. 4) configuration (OBJECT)
  250. Rules:
  251. + if there is one argument it had better be an object with atleast
  252. a `html` and `done` property (other properties are gravy)
  253. + arguments above are pulled out of the arguments and put into the
  254. config object that is returned
  255. */
  256. exports.env.processArguments = function(args) {
  257. if (!args || !args.length || args.length < 1) {
  258. throw new Error('No arguments passed to jsdom.env().');
  259. }
  260. var
  261. props = {
  262. 'html' : true,
  263. 'done' : true,
  264. 'scripts' : false,
  265. 'config' : false,
  266. 'url' : false // the URL for location.href if different from html
  267. },
  268. propKeys = Object.keys(props),
  269. config = {
  270. code : []
  271. },
  272. l = args.length
  273. ;
  274. if (l === 1) {
  275. config = args[0];
  276. } else {
  277. args.forEach(function(v) {
  278. var type = typeof v;
  279. if (!v) {
  280. return;
  281. }
  282. if (type === 'string' || v + '' === v) {
  283. config.html = v;
  284. } else if (type === 'object') {
  285. // Array
  286. if (v.length && v[0]) {
  287. config.scripts = v;
  288. } else {
  289. // apply missing required properties if appropriate
  290. propKeys.forEach(function(req) {
  291. if (typeof v[req] !== 'undefined' &&
  292. typeof config[req] === 'undefined') {
  293. config[req] = v[req];
  294. delete v[req];
  295. }
  296. });
  297. config.config = v;
  298. }
  299. } else if (type === 'function') {
  300. config.done = v;
  301. }
  302. });
  303. }
  304. propKeys.forEach(function(req) {
  305. var required = props[req];
  306. if (required && typeof config[req] === 'undefined') {
  307. throw new Error("jsdom.env requires a '" + req + "' argument");
  308. }
  309. });
  310. return config;
  311. };