index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. var sys = require('sys'),
  2. http = require('http'),
  3. URL = require('url'),
  4. HtmlToDom = require('./htmltodom').HtmlToDom,
  5. domToHtml = require('./domtohtml').domToHtml,
  6. htmlencoding = require('./htmlencoding'),
  7. HTMLEncode = htmlencoding.HTMLEncode,
  8. HTMLDecode = htmlencoding.HTMLDecode,
  9. jsdom = require('../../jsdom'),
  10. vm = require('vm');
  11. function NOT_IMPLEMENTED(target) {
  12. return function() {
  13. if (!jsdom.debugMode) {
  14. var trigger = target ? target.trigger : this.trigger;
  15. trigger.call(this, 'error', 'NOT IMPLEMENTED');
  16. }
  17. };
  18. }
  19. /**
  20. * Creates a window having a document. The document can be passed as option,
  21. * if omitted, a new document will be created.
  22. */
  23. exports.windowAugmentation = function(dom, options) {
  24. options = options || {};
  25. var window = exports.createWindow(dom, options);
  26. if (!options.document) {
  27. var browser = browserAugmentation(dom, options);
  28. if (options.features && options.features.QuerySelector) {
  29. require(__dirname + "/../selectors/index").applyQuerySelectorPrototype(browser);
  30. }
  31. options.document = (browser.HTMLDocument) ?
  32. new browser.HTMLDocument(options) :
  33. new browser.Document(options);
  34. options.document.write('<html><head></head><body></body></html>');
  35. }
  36. var doc = window.document = options.document;
  37. if (doc.addEventListener) {
  38. if (doc.readyState == 'complete') {
  39. var ev = doc.createEvent('HTMLEvents');
  40. ev.initEvent('load', false, false);
  41. window.dispatchEvent(ev);
  42. }
  43. else {
  44. doc.addEventListener('load', function(ev) {
  45. window.dispatchEvent(ev);
  46. });
  47. }
  48. }
  49. return window;
  50. };
  51. /**
  52. * Creates a document-less window.
  53. */
  54. exports.createWindow = function(dom, options) {
  55. var timers = [];
  56. function startTimer(startFn, stopFn, callback, ms) {
  57. var res = startFn(callback, ms);
  58. timers.push( [ res, stopFn ] );
  59. return res;
  60. }
  61. function stopTimer(id) {
  62. if (typeof id === 'undefined') {
  63. return;
  64. }
  65. for (var i in timers) {
  66. if (timers[i][0] === id) {
  67. timers[i][1].call(this, id);
  68. timers.splice(i, 1);
  69. break;
  70. }
  71. }
  72. }
  73. function stopAllTimers() {
  74. timers.forEach(function (t) {
  75. t[1].call(this, t[0]);
  76. });
  77. timers = [];
  78. }
  79. function DOMWindow(options) {
  80. this.frames = [this];
  81. this.contentWindow = this;
  82. this.window = this;
  83. this.self = this;
  84. var href = (options || {}).url || 'file://' + __filename;
  85. this.location = URL.parse(href);
  86. this.location.reload = NOT_IMPLEMENTED(this);
  87. this.location.replace = NOT_IMPLEMENTED(this);
  88. this.location.toString = function() {
  89. return href;
  90. };
  91. var window = this.console._window = this;
  92. if (options && options.document) {
  93. options.document.location = this.location;
  94. }
  95. this.addEventListener = function() {
  96. dom.Node.prototype.addEventListener.apply(window, arguments);
  97. };
  98. this.removeEventListener = function() {
  99. dom.Node.prototype.removeEventListener.apply(window, arguments);
  100. };
  101. this.dispatchEvent = function() {
  102. dom.Node.prototype.dispatchEvent.apply(window, arguments);
  103. };
  104. this.trigger = function(){
  105. dom.Node.prototype.trigger.apply(window.document, arguments);
  106. };
  107. this.setTimeout = function (fn, ms) { return startTimer(setTimeout, clearTimeout, fn, ms); };
  108. this.setInterval = function (fn, ms) { return startTimer(setInterval, clearInterval, fn, ms); };
  109. this.clearInterval = stopTimer;
  110. this.clearTimeout = stopTimer;
  111. this.__stopAllTimers = stopAllTimers;
  112. }
  113. DOMWindow.prototype = {
  114. __proto__: dom,
  115. get document() {
  116. return this._document;
  117. },
  118. set document(value) {
  119. this._document = value;
  120. if (value) {
  121. value.parentWindow = this;
  122. }
  123. },
  124. close : function() {
  125. if (this._document) {
  126. if (this._document.body) {
  127. this._document.body.innerHTML = "";
  128. }
  129. if (this._document.close) {
  130. this._document.close();
  131. }
  132. delete this._document;
  133. }
  134. stopAllTimers();
  135. if (this.__scriptContext) {
  136. delete this.__scriptContext;
  137. }
  138. },
  139. getComputedStyle: function(node) {
  140. var s = node.style,
  141. cs = {};
  142. for (var n in s) {
  143. cs[n] = s[n];
  144. }
  145. cs.__proto__ = {
  146. getPropertyValue: function(name) {
  147. return node.style[name];
  148. }
  149. };
  150. return cs;
  151. },
  152. console: {
  153. log: function(message) { this._window.trigger('log', message) },
  154. info: function(message) { this._window.trigger('info', message) },
  155. warn: function(message) { this._window.trigger('warn', message) },
  156. error: function(message) { this._window.trigger('error', message) }
  157. },
  158. navigator: {
  159. userAgent: 'Node.js (' + process.platform + '; U; rv:' + process.version + ')',
  160. appName: 'Node.js jsDom',
  161. platform: process.platform,
  162. appVersion: process.version
  163. },
  164. XMLHttpRequest: function XMLHttpRequest() {},
  165. name: 'nodejs',
  166. innerWidth: 1024,
  167. innerHeight: 768,
  168. length: 1,
  169. outerWidth: 1024,
  170. outerHeight: 768,
  171. pageXOffset: 0,
  172. pageYOffset: 0,
  173. screenX: 0,
  174. screenY: 0,
  175. screenLeft: 0,
  176. screenTop: 0,
  177. scrollX: 0,
  178. scrollY: 0,
  179. scrollTop: 0,
  180. scrollLeft: 0,
  181. alert: NOT_IMPLEMENTED(),
  182. blur: NOT_IMPLEMENTED(),
  183. confirm: NOT_IMPLEMENTED(),
  184. createPopup: NOT_IMPLEMENTED(),
  185. focus: NOT_IMPLEMENTED(),
  186. moveBy: NOT_IMPLEMENTED(),
  187. moveTo: NOT_IMPLEMENTED(),
  188. open: NOT_IMPLEMENTED(),
  189. print: NOT_IMPLEMENTED(),
  190. prompt: NOT_IMPLEMENTED(),
  191. resizeBy: NOT_IMPLEMENTED(),
  192. resizeTo: NOT_IMPLEMENTED(),
  193. scroll: NOT_IMPLEMENTED(),
  194. scrollBy: NOT_IMPLEMENTED(),
  195. scrollTo: NOT_IMPLEMENTED(),
  196. screen : {
  197. width : 0,
  198. height : 0
  199. },
  200. Image : NOT_IMPLEMENTED()
  201. };
  202. var window = new DOMWindow(options);
  203. // This is the last chance we have to properly update the window
  204. // so turn it into a context now.
  205. window = vm.createContext(window);
  206. return window;
  207. };
  208. //Caching for HTMLParser require. HUGE performace boost.
  209. /**
  210. * 5000 iterations
  211. * Without cache: ~1800+ms
  212. * With cache: ~80ms
  213. */
  214. var defaultParser = null;
  215. function getDefaultParser() {
  216. if (defaultParser === null) {
  217. try {
  218. defaultParser = require('htmlparser');
  219. }
  220. catch (e) {
  221. try {
  222. defaultParser = require('node-htmlparser/lib/node-htmlparser');
  223. }
  224. catch (e2) {
  225. defaultParser = undefined;
  226. }
  227. }
  228. }
  229. return defaultParser;
  230. }
  231. /**
  232. * Augments the given DOM by adding browser-specific properties and methods (BOM).
  233. * Returns the augmented DOM.
  234. */
  235. var browserAugmentation = exports.browserAugmentation = function(dom, options) {
  236. if (dom._augmented) {
  237. return dom;
  238. }
  239. if(!options) {
  240. options = {};
  241. }
  242. // set up html parser - use a provided one or try and load from library
  243. var htmltodom = new HtmlToDom(options.parser || getDefaultParser());
  244. if (!dom.HTMLDocument) {
  245. dom.HTMLDocument = dom.Document;
  246. }
  247. if (!dom.HTMLDocument.prototype.write) {
  248. dom.HTMLDocument.prototype.write = function(html) {
  249. this.innerHTML = html;
  250. };
  251. }
  252. dom.Element.prototype.getElementsByClassName = function(className) {
  253. function filterByClassName(child) {
  254. if (!child) {
  255. return false;
  256. }
  257. if (child.nodeType &&
  258. child.nodeType === dom.Node.ENTITY_REFERENCE_NODE)
  259. {
  260. child = child._entity;
  261. }
  262. var classString = child.className;
  263. if (classString) {
  264. var s = classString.split(" ");
  265. for (var i=0; i<s.length; i++) {
  266. if (s[i] === className) {
  267. return true;
  268. }
  269. }
  270. }
  271. return false;
  272. }
  273. return new dom.NodeList(this.ownerDocument || this, dom.mapper(this, filterByClassName));
  274. };
  275. dom.Element.prototype.__defineGetter__('sourceIndex', function() {
  276. /*
  277. * According to QuirksMode:
  278. * Get the sourceIndex of element x. This is also the index number for
  279. * the element in the document.getElementsByTagName('*') array.
  280. * http://www.quirksmode.org/dom/w3c_core.html#t77
  281. */
  282. var items = this.ownerDocument.getElementsByTagName('*'),
  283. len = items.length;
  284. for (var i = 0; i < len; i++) {
  285. if (items[i] === this) {
  286. return i;
  287. }
  288. }
  289. });
  290. dom.Document.prototype.__defineGetter__('outerHTML', function() {
  291. return domToHtml(this);
  292. });
  293. dom.Element.prototype.__defineGetter__('outerHTML', function() {
  294. return domToHtml(this);
  295. });
  296. dom.Element.prototype.__defineGetter__('innerHTML', function() {
  297. return domToHtml(this._childNodes, true);
  298. });
  299. dom.Element.prototype.__defineSetter__('doctype', function() {
  300. throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR);
  301. });
  302. dom.Element.prototype.__defineGetter__('doctype', function() {
  303. var r = null;
  304. if (this.nodeName == '#document') {
  305. if (this._doctype) {
  306. r = this._doctype;
  307. }
  308. }
  309. return r;
  310. });
  311. dom.Element.prototype.__defineSetter__('innerHTML', function(html) {
  312. //Check for lib first
  313. if (html === null) {
  314. return null;
  315. }
  316. //Clear the children first:
  317. var child;
  318. while ((child = this._childNodes[0])) {
  319. this.removeChild(child);
  320. }
  321. if (this.nodeName === '#document') {
  322. parseDocType(this, html);
  323. }
  324. var nodes = htmltodom.appendHtmlToElement(html, this);
  325. return html;
  326. });
  327. dom.Document.prototype.__defineGetter__('innerHTML', function() {
  328. return domToHtml(this._childNodes, true);
  329. });
  330. dom.Document.prototype.__defineSetter__('innerHTML', function(html) {
  331. //Check for lib first
  332. if (html === null) {
  333. return null;
  334. }
  335. //Clear the children first:
  336. var child;
  337. while ((child = this._childNodes[0])) {
  338. this.removeChild(child);
  339. }
  340. if (this.nodeName === '#document') {
  341. parseDocType(this, html);
  342. }
  343. var nodes = htmltodom.appendHtmlToElement(html, this);
  344. return html;
  345. });
  346. var DOC_HTML5 = /<!doctype html>/i,
  347. DOC_TYPE = /<!DOCTYPE (\w(.|\n)*)">/i;
  348. DOC_TYPE_START = '<!DOCTYPE ',
  349. DOC_TYPE_END = '">';
  350. function parseDocType(doc, html) {
  351. var publicID = '',
  352. systemID = '',
  353. fullDT = '',
  354. name = 'HTML',
  355. set = true,
  356. doctype = html.match(DOC_HTML5);
  357. //Default, No doctype === null
  358. doc._doctype = null;
  359. if (doctype && doctype[0]) { //Handle the HTML shorty doctype
  360. fullDT = doctype[0];
  361. } else { //Parse the doctype
  362. // find the start
  363. var start = html.indexOf(DOC_TYPE_START),
  364. end = html.indexOf(DOC_TYPE_END),
  365. docString;
  366. if (start < 0 || end < 0) {
  367. return;
  368. }
  369. docString = html.substr(start, (end-start)+DOC_TYPE_END.length);
  370. doctype = docString.replace(/[\n\r]/g,'').match(DOC_TYPE);
  371. if (!doctype) {
  372. return;
  373. }
  374. fullDT = doctype[0];
  375. doctype = doctype[1].split(' "');
  376. var _id1 = doctype.pop().replace(/"/g, ''),
  377. _id2 = doctype.pop().replace(/"/g, '');
  378. if (_id1.indexOf('-//') !== -1) {
  379. publicID = _id1;
  380. }
  381. if (_id2.indexOf('-//') !== -1) {
  382. publicID = _id2;
  383. }
  384. if (_id1.indexOf('://') !== -1) {
  385. systemID = _id1;
  386. }
  387. if (_id2.indexOf('://') !== -1) {
  388. systemID = _id2;
  389. }
  390. if (doctype.length) {
  391. doctype = doctype[0].split(' ');
  392. name = doctype[0].toUpperCase();
  393. }
  394. }
  395. doc._doctype = new dom.DOMImplementation().createDocumentType(name, publicID, systemID);
  396. doc._doctype._ownerDocument = doc;
  397. doc._doctype._fullDT = fullDT;
  398. doc._doctype.toString = function() {
  399. return this._fullDT;
  400. };
  401. }
  402. dom.Document.prototype.getElementsByClassName = function(className) {
  403. function filterByClassName(child) {
  404. if (!child) {
  405. return false;
  406. }
  407. if (child.nodeType &&
  408. child.nodeType === dom.Node.ENTITY_REFERENCE_NODE)
  409. {
  410. child = child._entity;
  411. }
  412. var classString = child.className;
  413. if (classString) {
  414. var s = classString.split(" ");
  415. for (var i=0; i<s.length; i++) {
  416. if (s[i] === className) {
  417. return true;
  418. }
  419. }
  420. }
  421. return false;
  422. }
  423. return new dom.NodeList(this.ownerDocument || this, dom.mapper(this, filterByClassName));
  424. };
  425. dom.Element.prototype.__defineGetter__('nodeName', function(val) {
  426. return this._nodeName.toUpperCase();
  427. });
  428. dom.Element.prototype.__defineGetter__('tagName', function(val) {
  429. var t = this._tagName.toUpperCase();
  430. //Document should not return a tagName
  431. if (this.nodeName === '#document') {
  432. t = null;
  433. }
  434. return t;
  435. });
  436. dom.Element.prototype.scrollTop = 0;
  437. dom.Element.prototype.scrollLeft = 0;
  438. dom.Document.prototype.__defineGetter__('parentWindow', function() {
  439. if (!this._parentWindow) {
  440. this._parentWindow = exports.windowAugmentation(dom, {document: this, url: this.URL});
  441. }
  442. return this._parentWindow;
  443. });
  444. dom.Document.prototype.__defineSetter__('parentWindow', function(window) {
  445. this._parentWindow = window;
  446. });
  447. dom.Document.prototype.__defineGetter__('defaultView', function() {
  448. return this.parentWindow;
  449. });
  450. dom._augmented = true;
  451. return dom;
  452. };