Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80684 views
1
var http = require('http'),
2
URL = require('url'),
3
HtmlToDom = require('./htmltodom').HtmlToDom,
4
domToHtml = require('./domtohtml').domToHtml,
5
htmlencoding = require('./htmlencoding'),
6
HTMLEncode = htmlencoding.HTMLEncode,
7
HTMLDecode = htmlencoding.HTMLDecode,
8
jsdom = require('../../jsdom'),
9
Location = require('./location'),
10
History = require('./history'),
11
NOT_IMPLEMENTED = require('./utils').NOT_IMPLEMENTED,
12
CSSStyleDeclaration = require('cssstyle').CSSStyleDeclaration,
13
toFileUrl = require('../utils').toFileUrl,
14
defineGetter = require('../utils').defineGetter,
15
defineSetter = require('../utils').defineSetter,
16
createFrom = require('../utils').createFrom,
17
Contextify = require('contextify');
18
19
function matchesDontThrow(el, selector) {
20
try {
21
return el.matchesSelector(selector);
22
} catch (e) {
23
return false;
24
}
25
}
26
27
/**
28
* Creates a window having a document. The document can be passed as option,
29
* if omitted, a new document will be created.
30
*/
31
exports.windowAugmentation = function(dom, options) {
32
options = options || {};
33
var window = exports.createWindow(dom, options);
34
35
if (!options.document) {
36
var browser = browserAugmentation(dom, options);
37
38
options.document = (browser.HTMLDocument) ?
39
new browser.HTMLDocument(options) :
40
new browser.Document(options);
41
42
43
44
options.document.write('<html><head></head><body></body></html>');
45
}
46
47
var doc = window.document = options.document;
48
49
if (doc.addEventListener) {
50
if (doc.readyState == 'complete') {
51
var ev = doc.createEvent('HTMLEvents');
52
ev.initEvent('load', false, false);
53
process.nextTick(function () {
54
window.dispatchEvent(ev);
55
});
56
}
57
else {
58
doc.addEventListener('load', function(ev) {
59
window.dispatchEvent(ev);
60
});
61
}
62
}
63
64
return window;
65
};
66
67
/**
68
* Creates a document-less window.
69
*/
70
exports.createWindow = function(dom, options) {
71
var timers = [];
72
var cssSelectorSplitRE = /((?:[^,"']|"[^"]*"|'[^']*')+)/;
73
74
function startTimer(startFn, stopFn, callback, ms) {
75
var res = startFn(callback, ms);
76
timers.push( [ res, stopFn ] );
77
return res;
78
}
79
80
function stopTimer(id) {
81
if (typeof id === 'undefined') {
82
return;
83
}
84
for (var i in timers) {
85
if (timers[i][0] === id) {
86
timers[i][1].call(this, id);
87
timers.splice(i, 1);
88
break;
89
}
90
}
91
}
92
93
function stopAllTimers() {
94
timers.forEach(function (t) {
95
t[1].call(this, t[0]);
96
});
97
timers = [];
98
}
99
100
function DOMWindow(options) {
101
var url = (options || {}).url || toFileUrl(__filename);
102
this.location = new Location(url, this);
103
this.history = new History(this);
104
105
this.console._window = this;
106
107
if (options && options.document) {
108
options.document.location = this.location;
109
}
110
111
this.addEventListener = function() {
112
dom.Node.prototype.addEventListener.apply(window, arguments);
113
};
114
this.removeEventListener = function() {
115
dom.Node.prototype.removeEventListener.apply(window, arguments);
116
};
117
this.dispatchEvent = function() {
118
dom.Node.prototype.dispatchEvent.apply(window, arguments);
119
};
120
this.raise = function(){
121
dom.Node.prototype.raise.apply(window.document, arguments);
122
};
123
124
this.setTimeout = function (fn, ms) { return startTimer(setTimeout, clearTimeout, fn, ms); };
125
this.setInterval = function (fn, ms) { return startTimer(setInterval, clearInterval, fn, ms); };
126
this.clearInterval = stopTimer;
127
this.clearTimeout = stopTimer;
128
this.__stopAllTimers = stopAllTimers;
129
}
130
131
DOMWindow.prototype = createFrom(dom || null, {
132
constructor: DOMWindow,
133
// This implements window.frames.length, since window.frames returns a
134
// self reference to the window object. This value is incremented in the
135
// HTMLFrameElement init function (see: level2/html.js).
136
_length : 0,
137
get length () {
138
return this._length;
139
},
140
close : function() {
141
// Recursively close child frame windows, then ourselves.
142
var currentWindow = this;
143
(function windowCleaner (window) {
144
var i;
145
// We could call window.frames.length etc, but window.frames just points
146
// back to window.
147
if (window.length > 0) {
148
for (i = 0; i < window.length; i++) {
149
windowCleaner(window[i]);
150
}
151
}
152
// We're already in our own window.close().
153
if (window !== currentWindow) {
154
window.close();
155
}
156
})(this);
157
158
if (this.document) {
159
if (this.document.body) {
160
this.document.body.innerHTML = "";
161
}
162
163
if (this.document.close) {
164
// We need to empty out the event listener array because
165
// document.close() causes 'load' event to re-fire.
166
this.document._listeners = [];
167
this.document.close();
168
}
169
delete this.document;
170
}
171
172
stopAllTimers();
173
// Clean up the window's execution context.
174
// dispose() is added by Contextify.
175
this.dispose();
176
},
177
getComputedStyle: function(node) {
178
var s = node.style,
179
cs = new CSSStyleDeclaration(),
180
forEach = Array.prototype.forEach;
181
182
function setPropertiesFromRule(rule) {
183
if (!rule.selectorText) {
184
return;
185
}
186
187
var selectors = rule.selectorText.split(cssSelectorSplitRE);
188
var matched = false;
189
selectors.forEach(function (selectorText) {
190
if (selectorText !== '' && selectorText !== ',' && !matched && matchesDontThrow(node, selectorText)) {
191
matched = true;
192
forEach.call(rule.style, function (property) {
193
cs.setProperty(property, rule.style.getPropertyValue(property), rule.style.getPropertyPriority(property));
194
});
195
}
196
});
197
}
198
199
forEach.call(node.ownerDocument.styleSheets, function (sheet) {
200
forEach.call(sheet.cssRules, function (rule) {
201
if (rule.media) {
202
if (Array.prototype.indexOf.call(rule.media, 'screen') !== -1) {
203
forEach.call(rule.cssRules, setPropertiesFromRule);
204
}
205
} else {
206
setPropertiesFromRule(rule);
207
}
208
});
209
});
210
211
forEach.call(s, function (property) {
212
cs.setProperty(property, s.getPropertyValue(property), s.getPropertyPriority(property));
213
});
214
215
return cs;
216
},
217
console: {
218
log: function(message) { this._window.raise('log', message) },
219
info: function(message) { this._window.raise('info', message) },
220
warn: function(message) { this._window.raise('warn', message) },
221
error: function(message) { this._window.raise('error', message) }
222
},
223
navigator: {
224
userAgent: 'Node.js (' + process.platform + '; U; rv:' + process.version + ')',
225
appName: 'Node.js jsDom',
226
platform: process.platform,
227
appVersion: process.version,
228
noUI: true
229
},
230
XMLHttpRequest: function() {
231
var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
232
var xhr = new XMLHttpRequest();
233
var lastUrl = '';
234
xhr._open = xhr.open;
235
xhr.open = function(method, url, async, user, password) {
236
url = URL.resolve(options.url, url);
237
lastUrl = url;
238
return xhr._open(method, url, async, user, password);
239
};
240
xhr._send = xhr.send;
241
xhr.send = function(data) {
242
if (window.document.cookie) {
243
var cookieDomain = window.document._cookieDomain;
244
var url = URL.parse(lastUrl);
245
var host = url.host.split(':')[0];
246
if (host.indexOf(cookieDomain, host.length - cookieDomain.length) !== -1) {
247
xhr.setDisableHeaderCheck(true);
248
xhr.setRequestHeader('cookie', window.document.cookie);
249
xhr.setDisableHeaderCheck(false);
250
}
251
}
252
return xhr._send(data);
253
};
254
return xhr;
255
},
256
257
name: 'nodejs',
258
innerWidth: 1024,
259
innerHeight: 768,
260
outerWidth: 1024,
261
outerHeight: 768,
262
pageXOffset: 0,
263
pageYOffset: 0,
264
screenX: 0,
265
screenY: 0,
266
screenLeft: 0,
267
screenTop: 0,
268
scrollX: 0,
269
scrollY: 0,
270
scrollTop: 0,
271
scrollLeft: 0,
272
alert: NOT_IMPLEMENTED(null, 'window.alert'),
273
blur: NOT_IMPLEMENTED(null, 'window.blur'),
274
confirm: NOT_IMPLEMENTED(null, 'window.confirm'),
275
createPopup: NOT_IMPLEMENTED(null, 'window.createPopup'),
276
focus: NOT_IMPLEMENTED(null, 'window.focus'),
277
moveBy: NOT_IMPLEMENTED(null, 'window.moveBy'),
278
moveTo: NOT_IMPLEMENTED(null, 'window.moveTo'),
279
open: NOT_IMPLEMENTED(null, 'window.open'),
280
print: NOT_IMPLEMENTED(null, 'window.print'),
281
prompt: NOT_IMPLEMENTED(null, 'window.prompt'),
282
resizeBy: NOT_IMPLEMENTED(null, 'window.resizeBy'),
283
resizeTo: NOT_IMPLEMENTED(null, 'window.resizeTo'),
284
scroll: NOT_IMPLEMENTED(null, 'window.scroll'),
285
scrollBy: NOT_IMPLEMENTED(null, 'window.scrollBy'),
286
scrollTo: NOT_IMPLEMENTED(null, 'window.scrollTo'),
287
screen : {
288
width : 0,
289
height : 0
290
},
291
Image : NOT_IMPLEMENTED(null, 'window.Image'),
292
293
// Note: these will not be necessary for newer Node.js versions, which have
294
// typed arrays in V8 and thus on every global object. (That is, in newer
295
// versions we'll get `ArrayBuffer` just as automatically as we get
296
// `Array`.) But to support older versions, we explicitly set them here.
297
Int8Array: global.Int8Array,
298
Int16Array: global.Int16Array,
299
Int32Array: global.Int32Array,
300
Float32Array: global.Float32Array,
301
Float64Array: global.Float64Array,
302
Uint8Array: global.Uint8Array,
303
Uint8ClampedArray: global.Uint8ClampedArray,
304
Uint16Array: global.Uint16Array,
305
Uint32Array: global.Uint32Array,
306
ArrayBuffer: global.ArrayBuffer
307
});
308
309
var window = new DOMWindow(options);
310
311
Contextify(window);
312
313
// We need to set up self references using Contextify's getGlobal() so that
314
// the global object identity is correct (window === this).
315
// See Contextify README for more info.
316
var windowGlobal = window.getGlobal();
317
318
// Set up the window as if it's a top level window.
319
// If it's not, then references will be corrected by frame/iframe code.
320
// Note: window.frames is maintained in the HTMLFrameElement init function.
321
window.window = window.frames
322
= window.self
323
= window.parent
324
= window.top = windowGlobal;
325
326
return window;
327
};
328
329
//Caching for HTMLParser require. HUGE performace boost.
330
/**
331
* 5000 iterations
332
* Without cache: ~1800+ms
333
* With cache: ~80ms
334
*/
335
// TODO: is this even needed in modern Node.js versions?
336
var defaultParser = null;
337
var getDefaultParser = exports.getDefaultParser = function () {
338
if (defaultParser === null) {
339
defaultParser = require('htmlparser2');
340
}
341
return defaultParser;
342
}
343
344
/**
345
* Export getter/setter of default parser to facilitate testing
346
* with different HTML parsers.
347
*/
348
exports.setDefaultParser = function (parser) {
349
if (typeof parser == 'object') {
350
defaultParser = parser;
351
} else if (typeof parser == 'string')
352
defaultParser = require(parser);
353
}
354
355
/**
356
* Augments the given DOM by adding browser-specific properties and methods (BOM).
357
* Returns the augmented DOM.
358
*/
359
var browserAugmentation = exports.browserAugmentation = function(dom, options) {
360
361
if(!options) {
362
options = {};
363
}
364
365
// set up html parser - use a provided one or try and load from library
366
var parser = options.parser || getDefaultParser();
367
368
if (dom._augmented && dom._parser === parser) {
369
return dom;
370
}
371
372
dom._parser = parser;
373
var htmltodom = new HtmlToDom(parser);
374
375
if (!dom.HTMLDocument) {
376
dom.HTMLDocument = dom.Document;
377
}
378
if (!dom.HTMLDocument.prototype.write) {
379
dom.HTMLDocument.prototype.write = function(html) {
380
this.innerHTML = html;
381
};
382
}
383
384
dom.Element.prototype.getElementsByClassName = function(className) {
385
386
function filterByClassName(child) {
387
if (!child) {
388
return false;
389
}
390
391
if (child.nodeType &&
392
child.nodeType === dom.Node.ENTITY_REFERENCE_NODE)
393
{
394
child = child._entity;
395
}
396
397
var classString = child.className;
398
if (classString) {
399
var s = classString.split(" ");
400
for (var i=0; i<s.length; i++) {
401
if (s[i] === className) {
402
return true;
403
}
404
}
405
}
406
return false;
407
}
408
409
return new dom.NodeList(this.ownerDocument || this, dom.mapper(this, filterByClassName));
410
};
411
412
defineGetter(dom.Element.prototype, 'sourceIndex', function() {
413
/*
414
* According to QuirksMode:
415
* Get the sourceIndex of element x. This is also the index number for
416
* the element in the document.getElementsByTagName('*') array.
417
* http://www.quirksmode.org/dom/w3c_core.html#t77
418
*/
419
var items = this.ownerDocument.getElementsByTagName('*'),
420
len = items.length;
421
422
for (var i = 0; i < len; i++) {
423
if (items[i] === this) {
424
return i;
425
}
426
}
427
});
428
429
defineGetter(dom.Document.prototype, 'outerHTML', function() {
430
return domToHtml(this, true);
431
});
432
433
defineGetter(dom.Element.prototype, 'outerHTML', function() {
434
return domToHtml(this, true);
435
});
436
437
defineGetter(dom.Element.prototype, 'innerHTML', function() {
438
if (/^(?:script|style)$/.test(this._tagName)) {
439
var type = this.getAttribute('type');
440
if (!type || /^text\//i.test(type) || /\/javascript$/i.test(type)) {
441
return domToHtml(this._childNodes, true, true);
442
}
443
}
444
445
return domToHtml(this._childNodes, true);
446
});
447
448
defineSetter(dom.Element.prototype, 'doctype', function() {
449
throw new dom.DOMException(dom.NO_MODIFICATION_ALLOWED_ERR);
450
});
451
defineGetter(dom.Element.prototype, 'doctype', function() {
452
var r = null;
453
if (this.nodeName == '#document') {
454
if (this._doctype) {
455
r = this._doctype;
456
}
457
}
458
return r;
459
});
460
461
defineSetter(dom.Element.prototype, 'innerHTML', function(html) {
462
//Clear the children first:
463
var child;
464
while ((child = this._childNodes[0])) {
465
this.removeChild(child);
466
}
467
468
if (this.nodeName === '#document') {
469
parseDocType(this, html);
470
}
471
if (html !== "" && html != null) {
472
htmltodom.appendHtmlToElement(html, this);
473
}
474
return html;
475
});
476
477
478
defineGetter(dom.Document.prototype, 'innerHTML', function() {
479
return domToHtml(this._childNodes, true);
480
});
481
482
defineSetter(dom.Document.prototype, 'innerHTML', function(html) {
483
//Clear the children first:
484
var child;
485
while ((child = this._childNodes[0])) {
486
this.removeChild(child);
487
}
488
489
if (this.nodeName === '#document') {
490
parseDocType(this, html);
491
}
492
if (html !== "" && html != null) {
493
htmltodom.appendHtmlToElement(html, this);
494
}
495
return html;
496
});
497
498
var DOC_HTML5 = /<!doctype html>/i,
499
DOC_TYPE = /<!DOCTYPE (\w(.|\n)*)">/i,
500
DOC_TYPE_START = '<!DOCTYPE ',
501
DOC_TYPE_END = '">';
502
503
function parseDocType(doc, html) {
504
var publicID = '',
505
systemID = '',
506
fullDT = '',
507
name = 'HTML',
508
set = true,
509
doctype = html.match(DOC_HTML5);
510
511
//Default, No doctype === null
512
doc._doctype = null;
513
514
if (doctype && doctype[0]) { //Handle the HTML shorty doctype
515
fullDT = doctype[0];
516
} else { //Parse the doctype
517
// find the start
518
var start = html.indexOf(DOC_TYPE_START),
519
end = html.indexOf(DOC_TYPE_END),
520
docString;
521
522
if (start < 0 || end < 0) {
523
return;
524
}
525
526
docString = html.substr(start, (end-start)+DOC_TYPE_END.length);
527
doctype = docString.replace(/[\n\r]/g,'').match(DOC_TYPE);
528
529
if (!doctype) {
530
return;
531
}
532
533
fullDT = doctype[0];
534
doctype = doctype[1].split(' "');
535
var _id1 = doctype.length ? doctype.pop().replace(/"/g, '') : '',
536
_id2 = doctype.length ? doctype.pop().replace(/"/g, '') : '';
537
538
if (_id1.indexOf('-//') !== -1) {
539
publicID = _id1;
540
}
541
if (_id2.indexOf('-//') !== -1) {
542
publicID = _id2;
543
}
544
if (_id1.indexOf('://') !== -1) {
545
systemID = _id1;
546
}
547
if (_id2.indexOf('://') !== -1) {
548
systemID = _id2;
549
}
550
if (doctype.length) {
551
doctype = doctype[0].split(' ');
552
name = doctype[0].toUpperCase();
553
}
554
}
555
doc._doctype = new dom.DOMImplementation().createDocumentType(name, publicID, systemID);
556
doc._doctype._ownerDocument = doc;
557
doc._doctype._fullDT = fullDT;
558
doc._doctype.toString = function() {
559
return this._fullDT;
560
};
561
}
562
563
dom.Document.prototype.getElementsByClassName = function(className) {
564
565
function filterByClassName(child) {
566
if (!child) {
567
return false;
568
}
569
570
if (child.nodeType &&
571
child.nodeType === dom.Node.ENTITY_REFERENCE_NODE)
572
{
573
child = child._entity;
574
}
575
576
var classString = child.className;
577
if (classString) {
578
var s = classString.split(" ");
579
for (var i=0; i<s.length; i++) {
580
if (s[i] === className) {
581
return true;
582
}
583
}
584
}
585
return false;
586
}
587
588
return new dom.NodeList(this.ownerDocument || this, dom.mapper(this, filterByClassName));
589
};
590
591
defineGetter(dom.Element.prototype, 'nodeName', function(val) {
592
return this._nodeName.toUpperCase();
593
});
594
595
defineGetter(dom.Element.prototype, 'tagName', function(val) {
596
var t = this._tagName.toUpperCase();
597
//Document should not return a tagName
598
if (this.nodeName === '#document') {
599
t = null;
600
}
601
return t;
602
});
603
604
dom.Element.prototype.scrollTop = 0;
605
dom.Element.prototype.scrollLeft = 0;
606
607
defineGetter(dom.Document.prototype, 'parentWindow', function() {
608
if (!this._parentWindow) {
609
this.parentWindow = exports.windowAugmentation(dom, {document: this, url: this.URL});
610
}
611
return this._parentWindow;
612
});
613
614
defineSetter(dom.Document.prototype, 'parentWindow', function(window) {
615
// Contextify does not support getters and setters, so we have to set them
616
// on the original object instead.
617
window._frame = function (name, frame) {
618
if (typeof frame === 'undefined') {
619
delete window[name];
620
} else {
621
defineGetter(window, name, function () { return frame.contentWindow; });
622
}
623
};
624
this._parentWindow = window.getGlobal();
625
});
626
627
defineGetter(dom.Document.prototype, 'defaultView', function() {
628
return this.parentWindow;
629
});
630
631
dom._augmented = true;
632
return dom;
633
};
634
635