Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80665 views
1
var fs = require('fs');
2
var path = require('path');
3
var URL = require('url');
4
var pkg = require('../package.json');
5
6
var toFileUrl = require('./jsdom/utils').toFileUrl;
7
var defineGetter = require('./jsdom/utils').defineGetter;
8
var defineSetter = require('./jsdom/utils').defineSetter;
9
var style = require('./jsdom/level2/style');
10
var features = require('./jsdom/browser/documentfeatures');
11
var dom = exports.dom = require('./jsdom/level3/index').dom;
12
var createWindow = exports.createWindow = require('./jsdom/browser/index').createWindow;
13
14
15
var request = function(options, cb) {
16
request = require('request');
17
return request(options, cb);
18
}
19
20
exports.defaultLevel = dom.level3.html;
21
exports.browserAugmentation = require('./jsdom/browser/index').browserAugmentation;
22
exports.windowAugmentation = require('./jsdom/browser/index').windowAugmentation;
23
24
// Proxy feature functions to features module.
25
['availableDocumentFeatures',
26
'defaultDocumentFeatures',
27
'applyDocumentFeatures'].forEach(function (propName) {
28
defineGetter(exports, propName, function () {
29
return features[propName];
30
});
31
defineSetter(exports, propName, function (val) {
32
return features[propName] = val;
33
});
34
});
35
36
exports.debugMode = false;
37
38
defineGetter(exports, 'version', function() {
39
return pkg.version;
40
});
41
42
exports.level = function (level, feature) {
43
if(!feature) {
44
feature = 'core';
45
}
46
47
return require('./jsdom/level' + level + '/' + feature).dom['level' + level][feature];
48
};
49
50
exports.jsdom = function (html, level, options) {
51
52
options = options || {};
53
if(typeof level == 'string') {
54
level = exports.level(level, 'html');
55
} else {
56
level = level || exports.defaultLevel;
57
}
58
59
if (!options.url) {
60
options.url = (module.parent.id === 'jsdom') ?
61
module.parent.parent.filename :
62
module.parent.filename;
63
options.url = options.url.replace(/\\/g, '/');
64
if (options.url[0] !== '/') {
65
options.url = '/' + options.url;
66
}
67
options.url = 'file://' + options.url;
68
}
69
70
var browser = exports.browserAugmentation(level, options),
71
doc = (browser.HTMLDocument) ?
72
new browser.HTMLDocument(options) :
73
new browser.Document(options);
74
75
require('./jsdom/selectors/index').applyQuerySelectorPrototype(level);
76
77
features.applyDocumentFeatures(doc, options.features);
78
79
if (typeof html === 'undefined' || html === null ||
80
(html.trim && !html.trim())) {
81
doc.write('<html><head></head><body></body></html>');
82
} else {
83
doc.write(html + '');
84
}
85
86
if (doc.close && !options.deferClose) {
87
doc.close();
88
}
89
90
// Kept for backwards-compatibility. The window is lazily created when
91
// document.parentWindow or document.defaultView is accessed.
92
doc.createWindow = function() {
93
// Remove ourself
94
if (doc.createWindow) {
95
delete doc.createWindow;
96
}
97
return doc.parentWindow;
98
};
99
100
return doc;
101
};
102
103
exports.html = function(html, level, options) {
104
html += '';
105
106
// TODO: cache a regex and use it here instead
107
// or make the parser handle it
108
var htmlLowered = html.toLowerCase();
109
110
// body
111
if (!~htmlLowered.indexOf('<body')) {
112
html = '<body>' + html + '</body>';
113
}
114
115
// html
116
if (!~htmlLowered.indexOf('<html')) {
117
html = '<html>' + html + '</html>';
118
}
119
return exports.jsdom(html, level, options);
120
};
121
122
exports.jQueryify = exports.jsdom.jQueryify = function (window /* path [optional], callback */) {
123
124
if (!window || !window.document) { return; }
125
126
var args = Array.prototype.slice.call(arguments),
127
callback = (typeof(args[args.length - 1]) === 'function') && args.pop(),
128
path,
129
jQueryTag = window.document.createElement('script');
130
jQueryTag.className = 'jsdom';
131
132
if (args.length > 1 && typeof(args[1] === 'string')) {
133
path = args[1];
134
}
135
136
var features = window.document.implementation._features;
137
138
window.document.implementation.addFeature('FetchExternalResources', ['script']);
139
window.document.implementation.addFeature('ProcessExternalResources', ['script']);
140
window.document.implementation.addFeature('MutationEvents', ['2.0']);
141
jQueryTag.src = path || 'http://code.jquery.com/jquery-latest.js';
142
window.document.body.appendChild(jQueryTag);
143
144
jQueryTag.onload = function() {
145
if (callback) {
146
callback(window, window.jQuery);
147
}
148
149
window.document.implementation._features = features;
150
};
151
152
return window;
153
};
154
155
156
exports.env = exports.jsdom.env = function () {
157
var config = getConfigFromArguments(arguments);
158
var callback = config.done;
159
160
if (config.file) {
161
fs.readFile(config.file, 'utf-8', function (err, text) {
162
if (err) {
163
return callback(err);
164
}
165
166
config.html = text;
167
processHTML(config);
168
});
169
} else if (config.html) {
170
processHTML(config);
171
} else if (config.url) {
172
handleUrl(config);
173
} else if (config.somethingToAutodetect) {
174
var url = URL.parse(config.somethingToAutodetect);
175
if (url.protocol && url.hostname) {
176
config.url = config.somethingToAutodetect;
177
handleUrl(config.somethingToAutodetect);
178
} else {
179
fs.readFile(config.somethingToAutodetect, 'utf-8', function (err, text) {
180
if (err) {
181
if (err.code === 'ENOENT' || err.code === 'ENAMETOOLONG') {
182
config.html = config.somethingToAutodetect;
183
processHTML(config);
184
} else {
185
callback(err);
186
}
187
} else {
188
config.html = text;
189
config.url = toFileUrl(config.somethingToAutodetect);
190
processHTML(config);
191
}
192
});
193
}
194
}
195
196
function handleUrl() {
197
var options = {
198
uri: config.url,
199
encoding: config.encoding || 'utf8',
200
headers: config.headers || {},
201
proxy: config.proxy || null,
202
jar: config.jar !== undefined ? config.jar : true
203
};
204
205
request(options, function (err, res, responseText) {
206
if (err) {
207
return callback(err);
208
}
209
210
// The use of `res.request.uri.href` ensures that `window.location.href`
211
// is updated when `request` follows redirects.
212
config.html = responseText;
213
config.url = res.request.uri.href;
214
processHTML(config);
215
});
216
}
217
};
218
219
function processHTML(config) {
220
var callback = config.done;
221
var options = {
222
features: config.features,
223
url: config.url,
224
parser: config.parser
225
};
226
227
if (config.document) {
228
options.referrer = config.document.referrer;
229
options.cookie = config.document.cookie;
230
options.cookieDomain = config.document.cookieDomain;
231
}
232
233
var window = exports.html(config.html, null, options).createWindow();
234
var features = JSON.parse(JSON.stringify(window.document.implementation._features));
235
236
var docsLoaded = 0;
237
var totalDocs = config.scripts.length + config.src.length;
238
var readyState = null;
239
var errors = [];
240
241
if (!window || !window.document) {
242
return callback(new Error('JSDOM: a window object could not be created.'));
243
}
244
245
window.document.implementation.addFeature('FetchExternalResources', ['script']);
246
window.document.implementation.addFeature('ProcessExternalResources', ['script']);
247
window.document.implementation.addFeature('MutationEvents', ['2.0']);
248
249
function scriptComplete() {
250
docsLoaded++;
251
252
if (docsLoaded >= totalDocs) {
253
window.document.implementation._features = features;
254
255
errors = errors.concat(window.document.errors || []);
256
if (errors.length === 0) {
257
errors = null;
258
}
259
260
process.nextTick(function() {
261
callback(errors, window);
262
});
263
}
264
}
265
266
function handleScriptError(e) {
267
if (!errors) {
268
errors = [];
269
}
270
errors.push(e.error || e.message);
271
272
// nextTick so that an exception within scriptComplete won't cause
273
// another script onerror (which would be an infinite loop)
274
process.nextTick(scriptComplete);
275
}
276
277
if (config.scripts.length > 0 || config.src.length > 0) {
278
config.scripts.forEach(function (scriptSrc) {
279
var script = window.document.createElement('script');
280
script.className = 'jsdom';
281
script.onload = scriptComplete;
282
script.onerror = handleScriptError;
283
script.src = scriptSrc;
284
285
try {
286
// protect against invalid dom
287
// ex: http://www.google.com/foo#bar
288
window.document.documentElement.appendChild(script);
289
} catch (e) {
290
handleScriptError(e);
291
}
292
});
293
294
config.src.forEach(function (scriptText) {
295
var script = window.document.createElement('script');
296
script.onload = scriptComplete;
297
script.onerror = handleScriptError;
298
script.text = scriptText;
299
300
window.document.documentElement.appendChild(script);
301
window.document.documentElement.removeChild(script);
302
});
303
} else {
304
scriptComplete();
305
}
306
}
307
308
function getConfigFromArguments(args, callback) {
309
var config = {};
310
if (typeof args[0] === 'object') {
311
var configToClone = args[0];
312
Object.keys(configToClone).forEach(function (key) {
313
config[key] = configToClone[key];
314
});
315
} else {
316
var stringToAutodetect = null;
317
318
Array.prototype.forEach.call(args, function (arg) {
319
switch (typeof arg) {
320
case 'string':
321
config.somethingToAutodetect = arg;
322
break;
323
case 'function':
324
config.done = arg;
325
break;
326
case 'object':
327
if (Array.isArray(arg)) {
328
config.scripts = arg;
329
} else {
330
extend(config, arg);
331
}
332
break;
333
}
334
});
335
}
336
337
if (!config.done) {
338
throw new Error('Must pass a "done" option or a callback to jsdom.env.');
339
}
340
341
if (!config.somethingToAutodetect && !config.html && !config.file && !config.url) {
342
throw new Error('Must pass a "html", "file", or "url" option, or a string, to jsdom.env');
343
}
344
345
config.scripts = ensureArray(config.scripts);
346
config.src = ensureArray(config.src);
347
348
config.features = config.features || {
349
FetchExternalResources: false,
350
ProcessExternalResources: false,
351
SkipExternalResources: false
352
};
353
354
if (!config.url && config.file) {
355
config.url = toFileUrl(config.file);
356
}
357
358
return config;
359
}
360
361
function ensureArray(value) {
362
var array = value || [];
363
if (typeof array === 'string') {
364
array = [array];
365
}
366
return array;
367
}
368
369
function extend(config, overrides) {
370
Object.keys(overrides).forEach(function (key) {
371
config[key] = overrides[key];
372
});
373
}
374
375