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