diff options
| author | nanalelfe <nargiza.nosirova@mail.utoronto.ca> | 2016-07-25 03:14:34 +0000 |
|---|---|---|
| committer | nanalelfe <nargiza.nosirova@mail.utoronto.ca> | 2016-07-25 03:14:34 +0000 |
| commit | 34f553940cd5e9fba5ca6062333f42ba741b700a (patch) | |
| tree | d806ac66b323e688dcf09ba78a197ab41b4bcb5a /node_modules/jsdom/lib | |
| parent | 482401fa5777b910b099f96b0abdadf1d4cfa54c (diff) | |
Fixed /send_message error/success message show up.
Diffstat (limited to 'node_modules/jsdom/lib')
35 files changed, 9999 insertions, 0 deletions
diff --git a/node_modules/jsdom/lib/README.md b/node_modules/jsdom/lib/README.md new file mode 100644 index 0000000..8f0856c --- /dev/null +++ b/node_modules/jsdom/lib/README.md @@ -0,0 +1,17 @@ +# Note: this codebase is in a transitional state + +We're slowly moving from a historical model based on layered "levels" of specs, to the [living standard model](https://wiki.whatwg.org/wiki/FAQ#What_does_.22Living_Standard.22_mean.3F) actually implemented by browsers. As such, the code is kind of a mess. + +**Summary**: new features go in `lib/jsdom/living` and follow the code style there; modifications to existing features will require some spelunking to find out what to modify in-place. + +--- + +A lot of the main implementation is in `lib/jsdom/level1` and `lib/jsdom/level2`. (That includes things that didn't appear in the original DOM Level 1 and Level 2 specs, just because the code was located there and we had to patch it.) We're trying to avoid adding new code there, but patching old code is often still required. + +New features generally go in the `lib/jsdom/living` folder, in nice small files, with a clear coding style enforced by JSHint. + +We're planning to fix this whole situation with a multi-stage process: + +- First, consolidate any leftovers in `lib/jsdom/browser` and `lib/jsdom/level3`, as well as the more substantial body of code in `lib/jsdom/level2`, into `lib/jsdom/level1`. This will contain the "historical" portion of the jsdom codebase. +- Then, embark on a major cleanup and refactoring effort, splitting out small pieces from `lib/jsdom/level1` and into `lib/jsdom/living`, cleaning up the code style and spec compliance as we go. +- Finally, collapse the silly directory hierarchy into something less nested. diff --git a/node_modules/jsdom/lib/jsdom.js b/node_modules/jsdom/lib/jsdom.js new file mode 100644 index 0000000..e801634 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom.js @@ -0,0 +1,379 @@ +var fs = require('fs'); +var path = require('path'); +var URL = require('url'); + +var toFileUrl = require('./jsdom/utils').toFileUrl; +var defineGetter = require('./jsdom/utils').defineGetter; +var defineSetter = require('./jsdom/utils').defineSetter; +var features = require('./jsdom/browser/documentfeatures'); +var dom = require('./jsdom/living'); +var browserAugmentation = require('./jsdom/browser/index').browserAugmentation; +var domToHtml = require('./jsdom/browser/domtohtml').domToHtml; +var VirtualConsole = require('./jsdom/virtual-console'); + +var canReadFilesFromFS = !!fs.readFile; // in a browserify environment, this isn't present + +var request = function() { // lazy loading request + request = require('request'); + return request.apply(undefined, arguments); +} + +exports.getVirtualConsole = function (window) { + return window._virtualConsole; +}; +exports.debugMode = false; + +// Proxy feature functions to features module. +['availableDocumentFeatures', + 'defaultDocumentFeatures', + 'applyDocumentFeatures'].forEach(function (propName) { + defineGetter(exports, propName, function () { + return features[propName]; + }); + defineSetter(exports, propName, function (val) { + return features[propName] = val; + }); +}); + +exports.jsdom = function (html, options) { + if (options === undefined) { + options = {}; + } + if (options.parsingMode === undefined || options.parsingMode === 'auto') { + options.parsingMode = 'html'; + } + + var browser = browserAugmentation(dom, options); + var doc = new browser.HTMLDocument(options); + + if (options.created) { + options.created(null, doc.parentWindow); + } + + features.applyDocumentFeatures(doc, options.features); + + if (html === undefined) { + html = ''; + } + html = String(html); + doc.write(html); + + if (doc.close && !options.deferClose) { + doc.close(); + } + + return doc; +}; + +exports.jQueryify = exports.jsdom.jQueryify = function (window, jqueryUrl, callback) { + if (!window || !window.document) { + return; + } + + var features = window.document.implementation._features; + window.document.implementation._addFeature('FetchExternalResources', ['script']); + window.document.implementation._addFeature('ProcessExternalResources', ['script']); + window.document.implementation._addFeature('MutationEvents', ['2.0']); + + var scriptEl = window.document.createElement('script'); + scriptEl.className = 'jsdom'; + scriptEl.src = jqueryUrl; + scriptEl.onload = scriptEl.onerror = function () { + window.document.implementation._features = features; + + if (callback) { + callback(window, window.jQuery); + } + }; + + window.document.body.appendChild(scriptEl); +}; + +exports.env = exports.jsdom.env = function () { + var config = getConfigFromArguments(arguments); + + if (config.file && canReadFilesFromFS) { + fs.readFile(config.file, 'utf-8', function (err, text) { + if (err) { + if (config.created) { + config.created(err); + } + if (config.done) { + config.done([err]); + } + return; + } + + setParsingModeFromExtension(config, config.file); + + config.html = text; + processHTML(config); + }); + } else if (config.html !== undefined) { + processHTML(config); + } else if (config.url) { + handleUrl(config); + } else if (config.somethingToAutodetect !== undefined) { + var url = URL.parse(config.somethingToAutodetect); + if (url.protocol && url.hostname) { + config.url = config.somethingToAutodetect; + handleUrl(config.somethingToAutodetect); + } else if (canReadFilesFromFS) { + fs.readFile(config.somethingToAutodetect, 'utf-8', function (err, text) { + if (err) { + // the toString() test is because in Node.js, there is no proper code for this. + // This is fixed in io.js: https://github.com/iojs/io.js/issues/517 so: + // TODO: remove when we start requiring io.js + if (err.code === 'ENOENT' || err.code === 'ENAMETOOLONG' + || (err.toString() == 'Error: Path must be a string without null bytes.') + ) { + config.html = config.somethingToAutodetect; + processHTML(config); + } else { + if (config.created) { + config.created(err); + } + if (config.done) { + config.done([err]); + } + } + } else { + setParsingModeFromExtension(config, config.somethingToAutodetect); + + config.html = text; + config.url = toFileUrl(config.somethingToAutodetect); + processHTML(config); + } + }); + } else { + config.html = config.somethingToAutodetect; + processHTML(config); + } + } + + function handleUrl() { + var options = { + uri: config.url, + encoding: config.encoding || 'utf8', + headers: config.headers || {}, + proxy: config.proxy || null, + jar: config.jar !== undefined ? config.jar : true + }; + + request(options, function (err, res, responseText) { + if (err) { + if (config.created) { + config.created(err); + } + if (config.done) { + config.done([err]); + } + return; + } + + // The use of `res.request.uri.href` ensures that `window.location.href` + // is updated when `request` follows redirects. + config.html = responseText; + config.url = res.request.uri.href; + + if (config.parsingMode === "auto" && ( + res.headers["content-type"] === "application/xml" || + res.headers["content-type"] === "text/xml" || + res.headers["content-type"] === "application/xhtml+xml")) { + config.parsingMode = "xml"; + } + + processHTML(config); + }); + } +}; + +exports.serializeDocument = function (doc) { + return domToHtml(doc, true); +}; + +function processHTML(config) { + var options = { + features: config.features, + url: config.url, + parser: config.parser, + parsingMode: config.parsingMode, + created: config.created, + resourceLoader: config.resourceLoader + }; + + if (config.document) { + options.referrer = config.document.referrer; + options.cookie = config.document.cookie; + options.cookieDomain = config.document.cookieDomain; + } + + var window = exports.jsdom(config.html, options).parentWindow; + var features = JSON.parse(JSON.stringify(window.document.implementation._features)); + + var docsLoaded = 0; + var totalDocs = config.scripts.length + config.src.length; + var readyState = null; + var errors = []; + + if (!window || !window.document) { + if (config.created) { + config.created(new Error('JSDOM: a window object could not be created.')); + } + if (config.done) { + config.done([new Error('JSDOM: a window object could not be created.')]); + } + return; + } + + window.document.implementation._addFeature('FetchExternalResources', ['script']); + window.document.implementation._addFeature('ProcessExternalResources', ['script']); + window.document.implementation._addFeature('MutationEvents', ['2.0']); + + function scriptComplete() { + docsLoaded++; + + if (docsLoaded >= totalDocs) { + window.document.implementation._features = features; + + errors = errors.concat(window.document.errors || []); + if (errors.length === 0) { + errors = null; + } + + process.nextTick(function() { + if (config.loaded) { + config.loaded(errors, window); + } + if (config.done) { + config.done(errors, window); + } + }); + } + } + + function handleScriptError(e) { + if (!errors) { + errors = []; + } + errors.push(e.error || e.message); + + // nextTick so that an exception within scriptComplete won't cause + // another script onerror (which would be an infinite loop) + process.nextTick(scriptComplete); + } + + if (config.scripts.length > 0 || config.src.length > 0) { + config.scripts.forEach(function (scriptSrc) { + var script = window.document.createElement('script'); + script.className = 'jsdom'; + script.onload = scriptComplete; + script.onerror = handleScriptError; + script.src = scriptSrc; + + try { + // protect against invalid dom + // ex: http://www.google.com/foo#bar + window.document.documentElement.appendChild(script); + } catch (e) { + handleScriptError(e); + } + }); + + config.src.forEach(function (scriptText) { + var script = window.document.createElement('script'); + script.onload = scriptComplete; + script.onerror = handleScriptError; + script.text = scriptText; + + window.document.documentElement.appendChild(script); + window.document.documentElement.removeChild(script); + }); + } else { + if (window.document.readyState === 'complete') { + scriptComplete(); + } else { + window.addEventListener('load', function() { + scriptComplete(); + }); + } + } +} + +function getConfigFromArguments(args, callback) { + var config = {}; + if (typeof args[0] === 'object') { + var configToClone = args[0]; + Object.keys(configToClone).forEach(function (key) { + config[key] = configToClone[key]; + }); + } else { + var stringToAutodetect = null; + + Array.prototype.forEach.call(args, function (arg) { + switch (typeof arg) { + case 'string': + config.somethingToAutodetect = arg; + break; + case 'function': + config.done = arg; + break; + case 'object': + if (Array.isArray(arg)) { + config.scripts = arg; + } else { + extend(config, arg); + } + break; + } + }); + } + + if (!config.done && !config.created && !config.loaded) { + throw new Error('Must pass a "created", "loaded", "done" option or a callback to jsdom.env.'); + } + + if (config.somethingToAutodetect === undefined && + config.html === undefined && !config.file && !config.url) { + throw new Error('Must pass a "html", "file", or "url" option, or a string, to jsdom.env'); + } + + config.scripts = ensureArray(config.scripts); + config.src = ensureArray(config.src); + config.parsingMode = config.parsingMode || "auto"; + + config.features = config.features || { + FetchExternalResources: false, + ProcessExternalResources: false, + SkipExternalResources: false + }; + + if (!config.url && config.file) { + config.url = toFileUrl(config.file); + } + + return config; +} + +function ensureArray(value) { + var array = value || []; + if (typeof array === 'string') { + array = [array]; + } + return array; +} + +function extend(config, overrides) { + Object.keys(overrides).forEach(function (key) { + config[key] = overrides[key]; + }); +} + +function setParsingModeFromExtension(config, filename) { + if (config.parsingMode === "auto") { + var ext = path.extname(filename); + if (ext === ".xhtml" || ext === ".xml") { + config.parsingMode = "xml"; + } + } +} diff --git a/node_modules/jsdom/lib/jsdom/browser/Window.js b/node_modules/jsdom/lib/jsdom/browser/Window.js new file mode 100644 index 0000000..1cee5cb --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/Window.js @@ -0,0 +1,291 @@ +"use strict"; + +var URL = require("url"); +var CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration; +var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; +var dom = require("../level1/core"); +var NOT_IMPLEMENTED = require("./utils").NOT_IMPLEMENTED; +var createFrom = require("../utils").createFrom; +var History = require("./history"); +var VirtualConsole = require("../virtual-console"); + +var cssSelectorSplitRE = /((?:[^,"']|"[^"]*"|'[^']*')+)/; + +function matchesDontThrow(el, selector) { + try { + return el.matches(selector); + } catch (e) { + return false; + } +} + +function startTimer(window, startFn, stopFn, callback, ms) { + var res = startFn(callback, ms); + window.__timers.push([res, stopFn]); + return res; +} + +function stopTimer(window, id) { + if (typeof id === "undefined") { + return; + } + for (var i in window.__timers) { + if (window.__timers[i][0] === id) { + window.__timers[i][1].call(window, id); + window.__timers.splice(i, 1); + break; + } + } +} + +function stopAllTimers(window) { + window.__timers.forEach(function (t) { + t[1].call(window, t[0]); + }); + window.__timers = []; +} + +function Window(document) { + this.__timers = []; + + // TODO: very little of this belongs on the instance; they should be prototype methods instead. + + var window = this; + this._document = document; + this.history = new History(this); + + this.addEventListener = function () { + dom.Node.prototype.addEventListener.apply(window, arguments); + }; + this.removeEventListener = function () { + dom.Node.prototype.removeEventListener.apply(window, arguments); + }; + this.dispatchEvent = function () { + dom.Node.prototype.dispatchEvent.apply(window, arguments); + }; + this.raise = function () { + dom.Node.prototype.raise.apply(window.document, arguments); + }; + + this.setTimeout = function (fn, ms) { return startTimer(window, setTimeout, clearTimeout, fn, ms); }; + this.setInterval = function (fn, ms) { return startTimer(window, setInterval, clearInterval, fn, ms); }; + this.clearInterval = stopTimer.bind(this, window); + this.clearTimeout = stopTimer.bind(this, window); + this.__stopAllTimers = stopAllTimers.bind(this, window); + this.Image = function (width, height) { + var element = window._document.createElement("img"); + element.width = width; + element.height = height; + return element; + }; + + this._virtualConsole = new VirtualConsole(); + + function wrapConsoleMethod(method) { + return function () { + var args = Array.prototype.slice.call(arguments); + window._virtualConsole.emit.apply(window._virtualConsole, [method].concat(args)); + }; + } + + this.console = { + assert: wrapConsoleMethod("assert"), + clear: wrapConsoleMethod("clear"), + count: wrapConsoleMethod("count"), + debug: wrapConsoleMethod("debug"), + error: wrapConsoleMethod("error"), + group: wrapConsoleMethod("group"), + groupCollapse: wrapConsoleMethod("groupCollapse"), + groupEnd: wrapConsoleMethod("groupEnd"), + info: wrapConsoleMethod("info"), + log: wrapConsoleMethod("log"), + table: wrapConsoleMethod("table"), + time: wrapConsoleMethod("time"), + timeEnd: wrapConsoleMethod("timeEnd"), + trace: wrapConsoleMethod("trace"), + warn: wrapConsoleMethod("warn") + }; + + this.XMLHttpRequest = function () { + var xhr = new XMLHttpRequest(); + var lastUrl = ""; + xhr._open = xhr.open; + xhr.open = function (method, url, async, user, password) { + url = URL.resolve(window.document.URL, url); + lastUrl = url; + return xhr._open(method, url, async, user, password); + }; + xhr._send = xhr.send; + xhr.send = function (data) { + if (window.document.cookie) { + var cookieDomain = window.document._cookieDomain; + var url = URL.parse(lastUrl); + var host = url.host.split(":")[0]; + if (host.indexOf(cookieDomain, host.length - cookieDomain.length) !== -1) { + xhr.setDisableHeaderCheck(true); + xhr.setRequestHeader("cookie", window.document.cookie); + xhr.setDisableHeaderCheck(false); + } + } + return xhr._send(data); + }; + return xhr; + }; +} + +Window.prototype = createFrom(dom || null, { + constructor: Window, + // This implements window.frames.length, since window.frames returns a + // self reference to the window object. This value is incremented in the + // HTMLFrameElement init function (see: level2/html.js). + _length: 0, + get length() { + return this._length; + }, + get document() { + return this._document; + }, + get location() { + return this._document._location; + }, + close: function () { + // Recursively close child frame windows, then ourselves. + var currentWindow = this; + (function windowCleaner(window) { + var i; + // We could call window.frames.length etc, but window.frames just points + // back to window. + if (window.length > 0) { + for (i = 0; i < window.length; i++) { + windowCleaner(window[i]); + } + } + // We"re already in our own window.close(). + if (window !== currentWindow) { + window.close(); + } + })(this); + + if (this._document) { + if (this._document.body) { + this._document.body.innerHTML = ""; + } + + if (this._document.close) { + // We need to empty out the event listener array because + // document.close() causes "load" event to re-fire. + this._document._listeners = []; + this._document.close(); + } + delete this._document; + } + + stopAllTimers(currentWindow); + // Clean up the window"s execution context. + // dispose() is added by Contextify. + this.dispose(); + }, + getComputedStyle: function (node) { + var s = node.style, + cs = new CSSStyleDeclaration(), + forEach = Array.prototype.forEach; + + function setPropertiesFromRule(rule) { + if (!rule.selectorText) { + return; + } + + var selectors = rule.selectorText.split(cssSelectorSplitRE); + var matched = false; + selectors.forEach(function (selectorText) { + if (selectorText !== "" && selectorText !== "," && !matched && matchesDontThrow(node, selectorText)) { + matched = true; + forEach.call(rule.style, function (property) { + cs.setProperty(property, rule.style.getPropertyValue(property), rule.style.getPropertyPriority(property)); + }); + } + }); + } + + forEach.call(node.ownerDocument.styleSheets, function (sheet) { + forEach.call(sheet.cssRules, function (rule) { + if (rule.media) { + if (Array.prototype.indexOf.call(rule.media, "screen") !== -1) { + forEach.call(rule.cssRules, setPropertiesFromRule); + } + } else { + setPropertiesFromRule(rule); + } + }); + }); + + forEach.call(s, function (property) { + cs.setProperty(property, s.getPropertyValue(property), s.getPropertyPriority(property)); + }); + + return cs; + }, + + // TODO: all of the below data properties should be getters; right now they are shared between Window instances + // which is of course bad. + navigator: { + get userAgent() { return "Node.js (" + process.platform + "; U; rv:" + process.version + ")"; }, + get appName() { return "Node.js jsDom"; }, + get platform() { return process.platform; }, + get appVersion() { return process.version; }, + noUI: true, + get cookieEnabled() { return true; } + }, + + name: "nodejs", + innerWidth: 1024, + innerHeight: 768, + outerWidth: 1024, + outerHeight: 768, + pageXOffset: 0, + pageYOffset: 0, + screenX: 0, + screenY: 0, + screenLeft: 0, + screenTop: 0, + scrollX: 0, + scrollY: 0, + scrollTop: 0, + scrollLeft: 0, + alert: NOT_IMPLEMENTED("window.alert"), + blur: NOT_IMPLEMENTED("window.blur"), + confirm: NOT_IMPLEMENTED("window.confirm"), + createPopup: NOT_IMPLEMENTED("window.createPopup"), + focus: NOT_IMPLEMENTED("window.focus"), + moveBy: NOT_IMPLEMENTED("window.moveBy"), + moveTo: NOT_IMPLEMENTED("window.moveTo"), + open: NOT_IMPLEMENTED("window.open"), + print: NOT_IMPLEMENTED("window.print"), + prompt: NOT_IMPLEMENTED("window.prompt"), + resizeBy: NOT_IMPLEMENTED("window.resizeBy"), + resizeTo: NOT_IMPLEMENTED("window.resizeTo"), + scroll: NOT_IMPLEMENTED("window.scroll"), + scrollBy: NOT_IMPLEMENTED("window.scrollBy"), + scrollTo: NOT_IMPLEMENTED("window.scrollTo"), + screen: { + width: 0, + height: 0 + }, + + // Note: these will not be necessary for newer Node.js versions, which have + // typed arrays in V8 and thus on every global object. (That is, in newer + // versions we"ll get `ArrayBuffer` just as automatically as we get + // `Array`.) But to support older versions, we explicitly set them here. + Int8Array: global.Int8Array, + Int16Array: global.Int16Array, + Int32Array: global.Int32Array, + Float32Array: global.Float32Array, + Float64Array: global.Float64Array, + Uint8Array: global.Uint8Array, + Uint8ClampedArray: global.Uint8ClampedArray, + Uint16Array: global.Uint16Array, + Uint32Array: global.Uint32Array, + ArrayBuffer: global.ArrayBuffer +}); + +module.exports = Window; diff --git a/node_modules/jsdom/lib/jsdom/browser/documentAdapter.js b/node_modules/jsdom/lib/jsdom/browser/documentAdapter.js new file mode 100644 index 0000000..7d66168 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/documentAdapter.js @@ -0,0 +1,62 @@ +//Tree traversing +exports.getFirstChild = function (node) { + return node.childNodes[0]; +}; + +exports.getChildNodes = function (node) { + return node.childNodes; +}; + +exports.getParentNode = function (node) { + return node.parentNode; +}; + +exports.getAttrList = function (node) { + return node.attributes; +}; + +//Node data +exports.getTagName = function (element) { + return element.tagName.toLowerCase(); +}; + +exports.getNamespaceURI = function (element) { + return element.namespaceURI || 'http://www.w3.org/1999/xhtml'; +}; + +exports.getTextNodeContent = function (textNode) { + return textNode.nodeValue; +}; + +exports.getCommentNodeContent = function (commentNode) { + return commentNode.nodeValue; +}; + +exports.getDocumentTypeNodeName = function (doctypeNode) { + return doctypeNode.name; +}; + +exports.getDocumentTypeNodePublicId = function (doctypeNode) { + return doctypeNode.publicId || null; +}; + +exports.getDocumentTypeNodeSystemId = function (doctypeNode) { + return doctypeNode.systemId || null; +}; + +//Node types +exports.isTextNode = function (node) { + return node.nodeName === '#text'; +}; + +exports.isCommentNode = function (node) { + return node.nodeName === '#comment'; +}; + +exports.isDocumentTypeNode = function (node) { + return node.nodeType === 10; +}; + +exports.isElementNode = function (node) { + return !!node.tagName; +}; diff --git a/node_modules/jsdom/lib/jsdom/browser/documentfeatures.js b/node_modules/jsdom/lib/jsdom/browser/documentfeatures.js new file mode 100644 index 0000000..2e562cb --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/documentfeatures.js @@ -0,0 +1,52 @@ +exports.availableDocumentFeatures = [ + 'FetchExternalResources', + 'ProcessExternalResources', + 'MutationEvents', + 'SkipExternalResources' +]; + +exports.defaultDocumentFeatures = { + "FetchExternalResources": ['script', 'link'/*, 'img', 'css', 'frame'*/], + "ProcessExternalResources": ['script'/*, 'frame', 'iframe'*/], + "MutationEvents": '2.0', + "SkipExternalResources": false +}; + +exports.applyDocumentFeatures = function(doc, features) { + var i, maxFeatures = exports.availableDocumentFeatures.length, + defaultFeatures = exports.defaultDocumentFeatures, + j, + k, + featureName, + featureSource; + + features = features || {}; + + for (i=0; i<maxFeatures; i++) { + featureName = exports.availableDocumentFeatures[i]; + if (typeof features[featureName] !== 'undefined') { + featureSource = features[featureName]; + // We have to check the lowercase version also because the Document feature + // methods convert everything to lowercase. + } else if (typeof features[featureName.toLowerCase()] !== 'undefined') { + featureSource = features[featureName.toLowerCase()]; + } else if (defaultFeatures[featureName]) { + featureSource = defaultFeatures[featureName]; + } else { + continue; + } + + doc.implementation._removeFeature(featureName); + + if (typeof featureSource !== 'undefined') { + if (featureSource instanceof Array) { + k = featureSource.length; + for (j=0; j<k; j++) { + doc.implementation._addFeature(featureName, featureSource[j]); + } + } else { + doc.implementation._addFeature(featureName, featureSource); + } + } + } +}; diff --git a/node_modules/jsdom/lib/jsdom/browser/domtohtml.js b/node_modules/jsdom/lib/jsdom/browser/domtohtml.js new file mode 100644 index 0000000..0c256c4 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/domtohtml.js @@ -0,0 +1,24 @@ +"use strict"; + +var parse5 = require('parse5'); +var serializer = new parse5.TreeSerializer(require('./documentAdapter')); + +exports.domToHtml = function(dom) { + if (dom._toArray) { + // node list + dom = dom._toArray(); + } + if (typeof dom.length !== "undefined") { + var ret = ""; + for (var i = 0, len = dom.length; i < len; i++) { + ret += dom[i].nodeType === dom.DOCUMENT_NODE ? + serializer.serialize(dom[i]) : + serializer.serialize({ childNodes: [dom[i]] }); + } + return ret; + } else { + return dom.nodeType === dom.DOCUMENT_NODE ? + serializer.serialize(dom) : + serializer.serialize({ childNodes: [dom] }); + } +}; diff --git a/node_modules/jsdom/lib/jsdom/browser/history.js b/node_modules/jsdom/lib/jsdom/browser/history.js new file mode 100644 index 0000000..674b4ad --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/history.js @@ -0,0 +1,94 @@ +"use strict"; +var URL = require("url"); +var resolveHref = require("../utils").resolveHref; + +function StateEntry(data, title, url) { + this.data = data; + this.title = title; + this.url = url; +} + +module.exports = History; + +function History(window) { + this._states = [new StateEntry(null, "", window.location._url.href)]; + this._index = 0; + this._window = window; + this._location = window.location; +} + +History.prototype = { + constructor: History, + + get length() { + return this._states.length; + }, + + get state() { + var state = this._states[this._index]; + return state ? state.data : null; + }, + + back: function () { + this.go(-1); + }, + + forward: function () { + this.go(1); + }, + + go: function (delta) { + if (typeof delta === "undefined" || delta === 0) { + this._location.reload(); + return; + } + + var newIndex = this._index + delta; + + if (newIndex < 0 || newIndex >= this.length) { + return; + } + + this._index = newIndex; + + var state = this._states[newIndex]; + + this._applyState(state); + this._signalPopstate(state); + }, + + pushState: function (data, title, url) { + var state = new StateEntry(data, title, url); + if (this._index + 1 !== this._states.length) { + this._states = this._states.slice(0, this._index + 1); + } + this._states.push(state); + this._applyState(state); + this._index++; + }, + + replaceState: function (data, title, url) { + var state = new StateEntry(data, title, url); + this._states[this._index] = state; + this._applyState(state); + }, + + _applyState: function (state) { + this._location._url = URL.parse(resolveHref(this._location._url.href, state.url)); + }, + + _signalPopstate: function(state) { + if (this._window.document) { + var ev = this._window.document.createEvent("HTMLEvents"); + ev.initEvent("popstate", false, false); + ev.state = state.data; + process.nextTick(function () { + this._window.dispatchEvent(ev); + }.bind(this)); + } + }, + + toString: function () { + return "[object History]"; + } +}; diff --git a/node_modules/jsdom/lib/jsdom/browser/htmltodom.js b/node_modules/jsdom/lib/jsdom/browser/htmltodom.js new file mode 100644 index 0000000..88908dc --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/htmltodom.js @@ -0,0 +1,231 @@ +var parse5 = require('parse5'); +var htmlparser2 = require('htmlparser2'); + +function HtmlToDom(parser, parsingMode) { + if (!parser) { + if (parsingMode === "xml") { + parser = htmlparser2; + } else { + parser = parse5; + } + } + + if (parser.DefaultHandler || (parser.Parser && parser.TreeAdapters)) { + + // Forgiving HTML parser + + if (parser.DefaultHandler){ + // fb55/htmlparser2 + + parser.ParseHtml = function(rawHtml) { + var handler = new parser.DefaultHandler(); + // Check if document is XML + var isXML = parsingMode === "xml"; + var parserInstance = new parser.Parser(handler, { + xmlMode: isXML, + lowerCaseTags: !isXML, + lowerCaseAttributeNames: !isXML, + decodeEntities: true + }); + + parserInstance.includeLocation = false; + parserInstance.parseComplete(rawHtml); + return handler.dom; + }; + } else if (parser.Parser && parser.TreeAdapters) { + parser.ParseHtml = function (rawHtml) { + if (parsingMode === 'xml') { + throw new Error('Can\'t parse XML with parse5, please use htmlparser2 instead.'); + } + var instance = new parser.Parser(parser.TreeAdapters.htmlparser2); + var dom = instance.parse(rawHtml); + return dom.children; + }; + } + + this.appendHtmlToElement = function(html, element) { + + if (typeof html !== 'string') { + html +=''; + } + + var parsed = parser.ParseHtml(html); + + for (var i = 0; i < parsed.length; i++) { + setChild(element, parsed[i]); + } + + return element; + }; + this.appendHtmlToDocument = this.appendHtmlToElement; + + if (parser.Parser && parser.TreeAdapters) { + this.appendHtmlToElement = function (html, element) { + + if (typeof html !== 'string') { + html += ''; + } + + var instance = new parser.Parser(parser.TreeAdapters.htmlparser2); + var parentElement = parser.TreeAdapters.htmlparser2.createElement(element.tagName.toLowerCase(), element.namespaceURI, []); + var dom = instance.parseFragment(html, parentElement); + var parsed = dom.children; + + for (var i = 0; i < parsed.length; i++) { + setChild(element, parsed[i]); + } + + return element; + }; + } + + } else if (parser.moduleName == 'HTML5') { /* HTML5 parser */ + this.appendHtmlToElement = function(html, element) { + + if (typeof html !== 'string') { + html += ''; + } + if (html.length > 0) { + if (element.nodeType == 9) { + new parser.Parser({document: element}).parse(html); + } + else { + var p = new parser.Parser({document: element.ownerDocument}); + p.parse_fragment(html, element); + } + } + }; + } else { + this.appendHtmlToElement = function () { + console.log(''); + console.log('###########################################################'); + console.log('# WARNING: No compatible HTML parser was given.'); + console.log('# Element.innerHTML setter support has been disabled'); + console.log('# Element.innerHTML getter support will still function'); + console.log('###########################################################'); + console.log(''); + }; + } +}; + +// utility function for forgiving parser +function setChild(parent, node) { + + var c, newNode, currentDocument = parent._ownerDocument || parent; + + switch (node.type) + { + case 'tag': + case 'script': + case 'style': + try { + newNode = currentDocument._createElementNoTagNameValidation(node.name); + newNode._namespaceURI = node.namespace || "http://www.w3.org/1999/xhtml"; + if (node.location) { + newNode.sourceLocation = node.location; + newNode.sourceLocation.file = parent.sourceLocation.file; + } + } catch (err) { + currentDocument.raise('error', 'invalid markup', { + exception: err, + node : node + }); + + return null; + } + break; + + case 'root': + // If we are in <template> then implicitly create #document-fragment for it's content + if(parent.tagName === 'TEMPLATE' && parent._namespaceURI === 'http://www.w3.org/1999/xhtml') { + newNode = currentDocument.createDocumentFragment(); + // Mark fragment as parser-created template content, so it will be accepted by appendChild() + newNode._templateContent = true; + } + break; + + case 'text': + // HTML entities should already be decoded by the parser, so no need to decode them + newNode = currentDocument.createTextNode(node.data); + break; + + case 'comment': + newNode = currentDocument.createComment(node.data); + break; + + case 'directive': + if (node.name[0] === '?' && node.name.toLowerCase() !== '?xml') { + var data = node.data.slice(node.name.length + 1, -1); + newNode = currentDocument.createProcessingInstruction(node.name.substring(1), data); + } else if (node.name.toLowerCase() === '!doctype') { + newNode = parseDocType(currentDocument, '<' + node.data + '>'); + } + break; + + default: + return null; + break; + } + + if (!newNode) + return null; + + newNode._localName = node.name; + + if (node.attribs) { + for (c in node.attribs) { + var prefix = node['x-attribsPrefix'] && node['x-attribsPrefix'][c] ? node['x-attribsPrefix'][c] + ':' : ''; + // catchin errors here helps with improperly escaped attributes + // but properly fixing parent should (can only?) be done in the htmlparser itself + try { + newNode._setAttributeNoValidation(prefix + c, node.attribs[c]); + newNode.attributes[prefix + c]._namespaceURI = node['x-attribsNamespace'][c] || null; + newNode.attributes[prefix + c]._prefix = node['x-attribsPrefix'][c] || null; + newNode.attributes[prefix + c]._localName = c; + } catch(e2) { /* noop */ } + } + } + + if (node.children) { + for (c = 0; c < node.children.length; c++) { + setChild(newNode, node.children[c]); + } + } + + try{ + return parent.appendChild(newNode); + }catch(err){ + currentDocument.raise('error', err.message, { + exception: err, + node : node + }); + return null; + } +} + +var HTML5_DOCTYPE = /<!doctype html>/i; +var PUBLIC_DOCTYPE = /<!doctype\s+([^\s]+)\s+public\s+"([^"]+)"\s+"([^"]+)"/i; +var SYSTEM_DOCTYPE = /<!doctype\s+([^\s]+)\s+system\s+"([^"]+)"/i; + +function parseDocType(doc, html) { + if (HTML5_DOCTYPE.test(html)) { + return doc.implementation.createDocumentType("html", "", ""); + } + + var publicPieces = PUBLIC_DOCTYPE.exec(html); + if (publicPieces) { + return doc.implementation.createDocumentType(publicPieces[1], publicPieces[2], publicPieces[3]); + } + + var systemPieces = SYSTEM_DOCTYPE.exec(html); + if (systemPieces) { + return doc.implementation.createDocumentType(systemPieces[1], systemPieces[2], ""); + } + + // Shouldn't get here (the parser shouldn't let us know about invalid doctypes), but our logic likely isn't + // real-world perfect, so let's fallback. + return doc.implementation.createDocumentType("html", "", ""); +} + + +exports.HtmlToDom = HtmlToDom; diff --git a/node_modules/jsdom/lib/jsdom/browser/index.js b/node_modules/jsdom/lib/jsdom/browser/index.js new file mode 100644 index 0000000..0d44dc1 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/index.js @@ -0,0 +1,85 @@ +var HtmlToDom = require('./htmltodom').HtmlToDom, + jsdom = require('../../jsdom'), + defineGetter = require('../utils').defineGetter, + defineSetter = require('../utils').defineSetter, + Contextify = require('contextify'), + Window = require('./Window'); + +function windowAugmentation(document) { + var window = createWindow(document); + + if (document.addEventListener) { + if (document.readyState == 'complete') { + var ev = document.createEvent('HTMLEvents'); + ev.initEvent('load', false, false); + process.nextTick(function () { + window.dispatchEvent(ev); + }); + } + else { + document.addEventListener('load', function(ev) { + window.dispatchEvent(ev); + }); + } + } + + return window; +}; + +function createWindow(document) { + var window = new Window(document); + + Contextify(window); + + // We need to set up self references using Contextify's getGlobal() so that + // the global object identity is correct (window === this). + // See Contextify README for more info. + var windowGlobal = window.getGlobal(); + + // Set up the window as if it's a top level window. + // If it's not, then references will be corrected by frame/iframe code. + // Note: window.frames is maintained in the HTMLFrameElement init function. + window.window = window.frames + = window.self + = window.parent + = window.top = windowGlobal; + + return window; +}; + +/** + * Augments the given DOM by adding browser-specific properties and methods (BOM). + * Returns the augmented DOM. + */ +// TODO: this function is HORIBBLE. It modifies the *shared* `dom` variable with document-specific stuff. +// We call it in `jsdom.jsdom`, i.e. per-document. The checks `if (dom._augment && ...)` just mean that it won't modify +// the global twice *for the same options*. Bad stuff. +// +// None of the properties set here should be on `dom`, really. +exports.browserAugmentation = function (dom, options) { + if (!options) { + options = {}; + } + + var parser = options.parser; + + if (dom._augmented && dom._parser === parser && dom._parsingMode === options.parsingMode) { + return dom; + } + + dom._parser = parser; + dom._parsingMode = options.parsingMode; + var htmltodom = new HtmlToDom(parser, options.parsingMode); + dom.Document.prototype._htmlToDom = htmltodom; + dom.Document.prototype._domImpl = dom.DOMImplementation; + + defineGetter(dom.Document.prototype, 'parentWindow', function() { + if (!this._parentWindow) { + this.parentWindow = windowAugmentation(this); + } + return this._parentWindow; + }); + + dom._augmented = true; + return dom; +}; diff --git a/node_modules/jsdom/lib/jsdom/browser/location.js b/node_modules/jsdom/lib/jsdom/browser/location.js new file mode 100644 index 0000000..92ece8c --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/location.js @@ -0,0 +1,103 @@ +"use strict"; + +var URL = require("url"); +var resolveHref = require("../utils").resolveHref; +var NOT_IMPLEMENTED = require("./utils").NOT_IMPLEMENTED; + +module.exports = Location; + +function Location(urlString, document) { + this._url = URL.parse(urlString); + this._document = document; +} + +Location.prototype = { + constructor: Location, + reload: function () { + NOT_IMPLEMENTED("location.reload", this._document)(); + }, + get protocol() { return this._url.protocol || ":"; }, + get host() { return this._url.host || ""; }, + get auth() { return this._url.auth; }, + get hostname() { return this._url.hostname || ""; }, + get origin() { + return ((this._url.protocol !== undefined && this._url.protocol !== null) ? + this._url.protocol + "//" : + this._url.protocol) + + this._url.host || ""; + }, + get port() { return this._url.port || ""; }, + get pathname() { return this._url.pathname || ""; }, + get href() { return this._url.href; }, + get hash() { return this._url.hash || ""; }, + get search() { return this._url.search || ""; }, + + set href(val) { + var oldUrl = this._url.href; + var oldProtocol = this._url.protocol; + var oldHost = this._url.host; + var oldPathname = this._url.pathname; + var oldHash = this._url.hash || ""; + + this._url = URL.parse(resolveHref(oldUrl, val)); + var newUrl = this._url.href; + var newProtocol = this._url.protocol; + var newHost = this._url.host; + var newPathname = this._url.pathname; + var newHash = this._url.hash || ""; + + if (oldProtocol === newProtocol && oldHost === newHost && oldPathname === newPathname && oldHash !== newHash) { + this._signalHashChange(oldUrl, newUrl); + } else { + NOT_IMPLEMENTED("location.href (no reload)", this._document)(); + } + }, + + set hash(val) { + var oldUrl = this._url.href; + var oldHash = this._url.hash || ""; + + if (val.lastIndexOf("#", 0) !== 0) { + val = "#" + val; + } + + this._url = URL.parse(resolveHref(oldUrl, val)); + var newUrl = this._url.href; + var newHash = this._url.hash || ""; + + if (oldHash !== newHash) { + this._signalHashChange(oldUrl, newUrl); + } + }, + + set search(val) { + var oldUrl = this._url.href; + var oldHash = this._url.hash || ""; + if (val.length) { + if (val.lastIndexOf("?", 0) !== 0) { + val = "?" + val; + } + this._url = URL.parse(resolveHref(oldUrl, val + oldHash)); + } else { + this._url = URL.parse(oldUrl.replace(/\?([^#]+)/, "")); + } + }, + + replace: function (val) { + this.href = val; + }, + + toString: function () { + return this._url.href; + }, + + _signalHashChange: function (oldUrl, newUrl) { + var ev = this._document.createEvent("HTMLEvents"); + ev.initEvent("hashchange", false, false); + ev.oldUrl = oldUrl; + ev.newUrl = newUrl; + process.nextTick(function () { + this._document.parentWindow.dispatchEvent(ev); + }.bind(this)); + } +}; diff --git a/node_modules/jsdom/lib/jsdom/browser/utils.js b/node_modules/jsdom/lib/jsdom/browser/utils.js new file mode 100644 index 0000000..87e952e --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/browser/utils.js @@ -0,0 +1,15 @@ +"use strict"; + +exports.NOT_IMPLEMENTED = function (nameForErrorMessage, target) { + return function () { + if (target === undefined) { + target = this; + } + + if (target && target.raise) { + target.raise("error", "NOT_IMPLEMENTED: " + nameForErrorMessage); + } else if (typeof console !== "undefined" && console.log) { + console.log(new Error("Called NOT_IMPLEMENTED without an element to raise on: " + nameForErrorMessage)); + } + }; +}; diff --git a/node_modules/jsdom/lib/jsdom/contextify-shim.js b/node_modules/jsdom/lib/jsdom/contextify-shim.js new file mode 100644 index 0000000..0bf22a9 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/contextify-shim.js @@ -0,0 +1,53 @@ +"use strict"; + +var acorn = require("acorn"); +var findGlobals = require("acorn-globals"); +var escodegen = require("escodegen"); + +module.exports = function (o) { + o.getGlobal = function () { + return o; + }; + + o.run = function (code, filename) { + var comments = [], tokens = []; + var ast = acorn.parse(code, { + ecmaVersion: 6, + allowReturnOutsideFunction: true, + ranges: true, + // collect comments in Esprima's format + onComment: comments, + // collect token ranges + onToken: tokens + }); + + // make sure we keep comments + escodegen.attachComments(ast, comments, tokens); + + var globals = findGlobals(ast); + for (var i = 0; i < globals.length; ++i) { + if (globals[i].name === "window") { + continue; + } + + var nodes = globals[i].nodes; + for (var j = 0; j < nodes.length; ++j) { + var type = nodes[j].type; + var name = nodes[j].name; + nodes[j].type = "MemberExpression"; + nodes[j].property = { + name: name, + type: type + }; + nodes[j].computed = false; + nodes[j].object = { + name: "window", + type: "Identifier" + }; + } + } + + code = escodegen.generate(ast, { comment: true }); + new Function("window", code + "\n//# sourceURL=" + filename).bind(o)(o); // jshint ignore:line + }; +}; diff --git a/node_modules/jsdom/lib/jsdom/level1/core.js b/node_modules/jsdom/lib/jsdom/level1/core.js new file mode 100644 index 0000000..26c8484 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level1/core.js @@ -0,0 +1,1794 @@ +/* + ServerJS Javascript DOM Level 1 +*/ +var inheritFrom = require("../utils").inheritFrom; +var domToHtml = require("../browser/domtohtml").domToHtml; +var defineGetter = require("../utils").defineGetter; +var memoizeQuery = require("../utils").memoizeQuery; +var validateName = require('../living/helpers/validate-names').name; +var Location = require("../browser/location"); + +// utility functions +var attachId = function(id,elm,doc) { + if (id && elm && doc) { + if (!doc._ids[id]) { + doc._ids[id] = []; + } + doc._ids[id].push(elm); + } +}; +var detachId = function(id,elm,doc) { + var elms, i; + if (id && elm && doc) { + if (doc._ids && doc._ids[id]) { + elms = doc._ids[id]; + for (i=0;i<elms.length;i++) { + if (elms[i] === elm) { + elms.splice(i,1); + i--; + } + } + if (elms.length === 0) { + delete doc._ids[id]; + } + } + } +}; + +function setInnerHTML(dom, node, html) { + //Clear the children first: + var child; + while ((child = node._childNodes[0])) { + node.removeChild(child); + } + + var isDoc = node.nodeName === '#document'; + if (html !== "" && html != null) { + if (isDoc) { + dom._htmlToDom.appendHtmlToDocument(html, node); + } else { + dom._htmlToDom.appendHtmlToElement(html, node); + } + } +} + +// TODO: move all of these to utils.js. Right now they are exposed on window, which is bizarre. +var core = module.exports = { + mapper: function(parent, filter, recursive) { + return function() { + return core.mapDOMNodes(parent, recursive !== false, filter); + }; + }, + + // Returns Array + mapDOMNodes : function(parent, recursive, callback) { + function visit(parent, result) { + return parent._childNodes.reduce(reducer, result); + } + + function reducer(array, child) { + if (callback(child)) { + array.push(child); + } + if (recursive && child._childNodes) { + visit(child, array); + } + return array; + } + + return visit(parent, []); + }, + + visitTree: function(root, callback) { + var cur = root; // TODO: Unroll this. + + function visit(el) { + if (el) { + callback(el); + if (el._childNodes) { + var i = 0, + children = el._childNodes, + l = children.length; + + for (i; i<l; i++) { + visit(children[i]); + } + } + } + } + visit(root); + }, + + markTreeReadonly: function(node) { + function markLevel(el) { + el._readonly = true; + // also mark attributes and their children read-only + if (el.attributes) { + var attributes = el.attributes, l = attributes.length, i=0; + attributes._readonly = true; + + for (i; i<l; i++) { + core.visitTree(attributes[i], markLevel); + } + } + } + + core.visitTree(node, markLevel); + } +}; + +// ExceptionCode +var INDEX_SIZE_ERR = core.INDEX_SIZE_ERR = 1, + DOMSTRING_SIZE_ERR = core.DOMSTRING_SIZE_ERR = 2, + HIERARCHY_REQUEST_ERR = core.HIERARCHY_REQUEST_ERR = 3, + WRONG_DOCUMENT_ERR = core.WRONG_DOCUMENT_ERR = 4, + INVALID_CHARACTER_ERR = core.INVALID_CHARACTER_ERR = 5, + NO_DATA_ALLOWED_ERR = core.NO_DATA_ALLOWED_ERR = 6, + NO_MODIFICATION_ALLOWED_ERR = core.NO_MODIFICATION_ALLOWED_ERR = 7, + NOT_FOUND_ERR = core.NOT_FOUND_ERR = 8, + NOT_SUPPORTED_ERR = core.NOT_SUPPORTED_ERR = 9, + INUSE_ATTRIBUTE_ERR = core.INUSE_ATTRIBUTE_ERR = 10, + INVALID_STATE_ERR = core.INVALID_STATE_ERR = 11, + SYNTAX_ERR = core.SYNTAX_ERR = 12, + INVALID_MODIFICATION_ERR = core.INVALID_MODIFICATION_ERR = 13, + NAMESPACE_ERR = core.NAMESPACE_ERR = 14, + INVALID_ACCESS_ERR = core.INVALID_ACCESS_ERR = 15, +// Node Types + ELEMENT_NODE = 1, + ATTRIBUTE_NODE = 2, + TEXT_NODE = 3, + CDATA_SECTION_NODE = 4, + ENTITY_REFERENCE_NODE = 5, + ENTITY_NODE = 6, + PROCESSING_INSTRUCTION_NODE = 7, + COMMENT_NODE = 8, + DOCUMENT_NODE = 9, + DOCUMENT_TYPE_NODE = 10, + DOCUMENT_FRAGMENT_NODE = 11, + NOTATION_NODE = 12; + +var messages = core.exceptionMessages = { }; +messages[INDEX_SIZE_ERR] = "Index size error"; +messages[DOMSTRING_SIZE_ERR] = "DOMString size error"; +messages[HIERARCHY_REQUEST_ERR] = "Hierarchy request error"; +messages[WRONG_DOCUMENT_ERR] = "Wrong document"; +messages[INVALID_CHARACTER_ERR] = "Invalid character"; +messages[NO_DATA_ALLOWED_ERR] = "No data allowed"; +messages[NO_MODIFICATION_ALLOWED_ERR] = "No modification allowed"; +messages[NOT_FOUND_ERR] = "Not found"; +messages[NOT_SUPPORTED_ERR] = "Not supported"; +messages[INUSE_ATTRIBUTE_ERR] = "Attribute in use"; +messages[NAMESPACE_ERR] = "Invalid namespace"; + +core.DOMException = function DOMException(code, message) { + Error.call(this, core.exceptionMessages[code]); + this.message = core.exceptionMessages[code]; + this.code = code; + + if (message) { + this.message = this.message + ": " + message; + } + + if (Error.captureStackTrace) { + + Error.captureStackTrace(this, DOMException); + } +}; + +core.DOMException.INDEX_SIZE_ERR = INDEX_SIZE_ERR; +core.DOMException.DOMSTRING_SIZE_ERR = DOMSTRING_SIZE_ERR; +core.DOMException.HIERARCHY_REQUEST_ERR = HIERARCHY_REQUEST_ERR; +core.DOMException.WRONG_DOCUMENT_ERR = WRONG_DOCUMENT_ERR; +core.DOMException.INVALID_CHARACTER_ERR = INVALID_CHARACTER_ERR; +core.DOMException.NO_DATA_ALLOWED_ERR = NO_DATA_ALLOWED_ERR; +core.DOMException.NO_MODIFICATION_ALLOWED_ERR = NO_MODIFICATION_ALLOWED_ERR; +core.DOMException.NOT_FOUND_ERR = NOT_FOUND_ERR; +core.DOMException.NOT_SUPPORTED_ERR = NOT_SUPPORTED_ERR; +core.DOMException.INUSE_ATTRIBUTE_ERR = INUSE_ATTRIBUTE_ERR; +core.DOMException.INVALID_STATE_ERR = INVALID_STATE_ERR; +core.DOMException.SYNTAX_ERR = SYNTAX_ERR; +core.DOMException.INVALID_MODIFICATION_ERR = INVALID_MODIFICATION_ERR; +core.DOMException.NAMESPACE_ERR = NAMESPACE_ERR; +core.DOMException.INVALID_ACCESS_ERR = INVALID_ACCESS_ERR; + +inheritFrom(Error, core.DOMException, { + name: "DOMException", + INDEX_SIZE_ERR : INDEX_SIZE_ERR, + DOMSTRING_SIZE_ERR : DOMSTRING_SIZE_ERR, + HIERARCHY_REQUEST_ERR : HIERARCHY_REQUEST_ERR, + WRONG_DOCUMENT_ERR : WRONG_DOCUMENT_ERR, + INVALID_CHARACTER_ERR : INVALID_CHARACTER_ERR, + NO_DATA_ALLOWED_ERR : NO_DATA_ALLOWED_ERR, + NO_MODIFICATION_ALLOWED_ERR : NO_MODIFICATION_ALLOWED_ERR, + NOT_FOUND_ERR : NOT_FOUND_ERR, + NOT_SUPPORTED_ERR : NOT_SUPPORTED_ERR, + INUSE_ATTRIBUTE_ERR : INUSE_ATTRIBUTE_ERR, + INVALID_STATE_ERR : INVALID_STATE_ERR, + SYNTAX_ERR : SYNTAX_ERR, + INVALID_MODIFICATION_ERR : INVALID_MODIFICATION_ERR, + NAMESPACE_ERR : NAMESPACE_ERR, + INVALID_ACCESS_ERR : INVALID_ACCESS_ERR +}); + +core.NodeList = function NodeList(element, query) { + if (!query) { + // Non-live NodeList + if (Array.isArray(element)) { + Array.prototype.push.apply(this, element); + } + Object.defineProperties(this, { + _length: {value: element ? element.length : 0, writable:true} + }); + } else { + Object.defineProperties(this, { + _element: {value: element}, + _query: {value: query}, + _snapshot: {writable: true}, + _length: {value: 0, writable: true}, + _version: {value: -1, writable: true} + }); + this._update(); + } +}; + +function lengthFromProperties(arrayLike) { + var max = -1; + var keys = Object.keys(arrayLike); + var highestKeyIndex = keys.length - 1; + + // abuses a v8 implementation detail for a very fast case + // (if this implementation detail changes, this method will still + // return correct results) + if (highestKeyIndex == keys[highestKeyIndex]) { // not === + return keys.length; + } + + for (var i = highestKeyIndex; i >= 0 ; --i) { + var asNumber = + keys[i]; + + if (!isNaN(asNumber) && asNumber > max) { + max = asNumber; + } + } + return max + 1; +} +core.NodeList.prototype = { + _update: function() { + var i; + + if (!this._element) { + this._length = lengthFromProperties(this); + } else { + if (this._version < this._element._version) { + var nodes = this._snapshot = this._query(); + this._resetTo(nodes); + this._version = this._element._version; + } + } + }, + _resetTo: function(array) { + var startingLength = lengthFromProperties(this); + for (var i = 0; i < startingLength; ++i) { + delete this[i]; + } + + for (var j = 0; j < array.length; ++j) { + this[j] = array[j]; + } + this._length = array.length; + }, + _toArray: function() { + if (this._element) { + this._update(); + return this._snapshot; + } + + return Array.prototype.slice.call(this); + }, + get length() { + this._update(); + return this._length || 0; + }, + set length(length) { + return this._length; + }, + item: function(index) { + this._update(); + return this[index] || null; + }, + toString: function() { + return '[ jsdom NodeList ]: contains ' + this.length + ' items'; + } +}; +Object.defineProperty(core.NodeList.prototype, 'constructor', { + value: core.NodeList, + writable: true, + configurable: true +}); + +core.DOMImplementation = function DOMImplementation(document, /* Object */ features) { + this._ownerDocument = document; + this._features = {}; + + if (features) { + for (var feature in features) { + if (features.hasOwnProperty(feature)) { + this._addFeature(feature.toLowerCase(), features[feature]); + } + } + } +}; + +core.DOMImplementation.prototype = { + // All of these are legacy, left because jsdom uses them internally :(. jsdom confused the idea of browser features + // and jsdom features + _removeFeature : function(feature, version) { + feature = feature.toLowerCase(); + if (this._features[feature]) { + if (version) { + var j = 0, + versions = this._features[feature], + l = versions.length; + + for (j; j<l; j++) { + if (versions[j] === version) { + versions.splice(j,1); + return; + } + } + } else { + delete this._features[feature]; + } + } + }, + + _addFeature: function(feature, version) { + feature = feature.toLowerCase(); + + if (version) { + + if (!this._features[feature]) { + this._features[feature] = []; + } + + if (version instanceof Array) { + Array.prototype.push.apply(this._features[feature], version); + } else { + this._features[feature].push(version); + } + } + }, + + // The real hasFeature is in living/dom-implementation.js, and returns true always. + // This one is used internally + _hasFeature: function(/* string */ feature, /* string */ version) { + feature = (feature) ? feature.toLowerCase() : ''; + var versions = (this._features[feature]) ? + this._features[feature] : + false; + + if (!version && versions.length && versions.length > 0) { + return true; + } else if (typeof versions === 'string') { + return versions === version; + } else if (versions.indexOf && versions.length > 0) { + for (var i = 0; i < versions.length; i++) { + var found = versions[i] instanceof RegExp ? + versions[i].test(version) : + versions[i] === version; + if (found) { return true; } + } + return false; + } else { + return false; + } + } +}; + + +var attrCopy = function(src, dest, fn) { + if (src.attributes) { + var attrs = src.attributes, i, l = attrs.length, attr, copied; + for (i=0;i<l;i++) { + attr = attrs[i]; + // skip over default attributes + if (!attr.specified) { + continue; + } + // TODO: consider duplicating this code and moving it into level2/core + if (attr.namespaceURI) { + dest.setAttributeNS(attr.namespaceURI, + attr.name, + attr.value); + var localName = attr.name.split(':').pop(); + copied = dest.getAttributeNodeNS(attr.namespaceURI, localName); + } else { + dest.setAttribute(attr.name, attr.value); + copied = dest.getAttributeNode(attr.name); + } + if (typeof fn == "function") { + fn(attr, copied); + } + + } + } + return dest; +}; + +core.Node = function Node(ownerDocument) { + this._childNodes = []; + this._childNodesList = null; + this._ownerDocument = ownerDocument; + this._attributes = new AttributeList(ownerDocument, this); + this._childrenList = null; + this._version = 0; + this._parentNode = null; + this._memoizedQueries = {}; + this._readonly = false; +}; + +core.Node.ELEMENT_NODE = ELEMENT_NODE; +core.Node.ATTRIBUTE_NODE = ATTRIBUTE_NODE; +core.Node.TEXT_NODE = TEXT_NODE; +core.Node.CDATA_SECTION_NODE = CDATA_SECTION_NODE; +core.Node.ENTITY_REFERENCE_NODE = ENTITY_REFERENCE_NODE; +core.Node.ENTITY_NODE = ENTITY_NODE; +core.Node.PROCESSING_INSTRUCTION_NODE = PROCESSING_INSTRUCTION_NODE; +core.Node.COMMENT_NODE = COMMENT_NODE; +core.Node.DOCUMENT_NODE = DOCUMENT_NODE; +core.Node.DOCUMENT_TYPE_NODE = DOCUMENT_TYPE_NODE; +core.Node.DOCUMENT_FRAGMENT_NODE = DOCUMENT_FRAGMENT_NODE; +core.Node.NOTATION_NODE = NOTATION_NODE; + +core.Node.prototype = { + ELEMENT_NODE : ELEMENT_NODE, + ATTRIBUTE_NODE : ATTRIBUTE_NODE, + TEXT_NODE : TEXT_NODE, + CDATA_SECTION_NODE : CDATA_SECTION_NODE, + ENTITY_REFERENCE_NODE : ENTITY_REFERENCE_NODE, + ENTITY_NODE : ENTITY_NODE, + PROCESSING_INSTRUCTION_NODE : PROCESSING_INSTRUCTION_NODE, + COMMENT_NODE : COMMENT_NODE, + DOCUMENT_NODE : DOCUMENT_NODE, + DOCUMENT_TYPE_NODE : DOCUMENT_TYPE_NODE, + DOCUMENT_FRAGMENT_NODE : DOCUMENT_FRAGMENT_NODE, + NOTATION_NODE : NOTATION_NODE, + + get children() { + if (!this._childrenList) { + var self = this; + this._childrenList = new core.NodeList(this, function() { + return self._childNodes.filter(function(node) { + return node.tagName; + }); + }); + } + this._childrenList._update(); + return this._childrenList; + }, + get nodeValue() { + if (this.nodeType === core.Node.TEXT_NODE || + this.nodeType === core.Node.COMMENT_NODE || + this.nodeType === core.Node.PROCESSING_INSTRUCTION_NODE) { + return this._data; + } + + return null; + }, + set nodeValue(value) { + if (this.nodeType === core.Node.TEXT_NODE || + this.nodeType === core.Node.COMMENT_NODE || + this.nodeType === core.Node.PROCESSING_INSTRUCTION_NODE) { + this.replaceData(0, this.length, value); + } + }, + get parentNode() { return this._parentNode;}, + + get nodeName() { + switch (this.nodeType) { + case ELEMENT_NODE: + return this.tagName; + case TEXT_NODE: + return "#text"; + case PROCESSING_INSTRUCTION_NODE: + return this.target; + case COMMENT_NODE: + return "#comment"; + case DOCUMENT_NODE: + return "#document"; + case DOCUMENT_TYPE_NODE: + return this.name; + case DOCUMENT_FRAGMENT_NODE: + return "#document-fragment"; + case ATTRIBUTE_NODE: + // TODO: remove this; attributes should not be nodes and should not have a nodeName property + // Removing it breaks some legit-seeming xpath tests :-/ + return this.name; + } + }, + set nodeName(unused) { throw new core.DOMException();}, + get firstChild() { + return this._childNodes.length > 0 ? this._childNodes[0] : null; + }, + get ownerDocument() { return this._ownerDocument;}, + get readonly() { return this._readonly;}, + + get lastChild() { + var len = this._childNodes.length; + return len > 0 ? this._childNodes[len -1] : null; + }, + + get childNodes() { + if (!this._childNodesList) { + var self = this; + this._childNodesList = new core.NodeList(this, function() { + return self._childNodes.slice(); + }); + } + this._childNodesList._update(); + return this._childNodesList; + }, + set childNodes(unused) { throw new core.DOMException();}, + + _indexOf: function(/*Node*/ child) { + return this._childNodes.indexOf(child); + }, + + get nextSibling() { + // find this node's index in the parentNode, add one and call it a day + if (!this._parentNode || !this._parentNode._indexOf) { + return null; + } + + var index = this._parentNode._indexOf(this); + + if (index == -1 || index+1 >= this._parentNode._childNodes.length) { + return null; + } + + return this._parentNode._childNodes[index+1] || null; + }, + set nextSibling(unused) { throw new core.DOMException();}, + + get previousSibling() { + if (!this._parentNode || !this._parentNode._indexOf) { + return null; + } + + var index = this._parentNode._indexOf(this); + + if (index == -1 || index-1 < 0) { + return null; + } + + return this._parentNode._childNodes[index-1] || null; + }, + set previousSibling(unused) { throw new core.DOMException();}, + + /* returns Node */ + insertBefore : function(/* Node */ newChild, /* Node*/ refChild) { + if (this._readonly === true) { + throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR, 'Attempting to modify a read-only node'); + } + + // Adopt unowned children, for weird nodes like DocumentType + if (!newChild._ownerDocument) newChild._ownerDocument = this._ownerDocument; + + // TODO - if (!newChild) then? + if (!(this instanceof core.Document) && newChild._ownerDocument !== this._ownerDocument) { + throw new core.DOMException(WRONG_DOCUMENT_ERR); + } + + if (newChild.nodeType && newChild.nodeType === ATTRIBUTE_NODE) { + throw new core.DOMException(HIERARCHY_REQUEST_ERR); + } + + // search for parents matching the newChild + var current = this; + do { + if (current === newChild) { + throw new core.DOMException(HIERARCHY_REQUEST_ERR); + } + } while((current = current._parentNode)); + + // fragments are merged into the element (except parser-created fragments in <template>) + if (newChild.nodeType === DOCUMENT_FRAGMENT_NODE && !newChild._templateContent) { + var tmpNode, i = newChild._childNodes.length; + while (i-- > 0) { + tmpNode = newChild.removeChild(newChild.firstChild); + this.insertBefore(tmpNode, refChild); + } + } else if (newChild === refChild) { + return newChild; + } else { + // if the newChild is already in the tree elsewhere, remove it first + if (newChild._parentNode) { + newChild._parentNode.removeChild(newChild); + } + + if (refChild == null) { + this._childNodes.push(newChild); + } else { + var refChildIndex = this._indexOf(refChild); + if (refChildIndex == -1) { + throw new core.DOMException(NOT_FOUND_ERR); + } + this._childNodes.splice(refChildIndex, 0, newChild); + } + + newChild._parentNode = this; + if (this._attached && newChild._attach) { + newChild._attach(); + } + + this._modified(); + this._descendantAdded(this, newChild); + } + + return newChild; + }, // raises(DOMException); + + _modified: function() { + this._version++; + if (this._ownerDocument) { + this._ownerDocument._version++; + } + + if (this._childrenList) { + this._childrenList._update(); + } + this._clearMemoizedQueries() + }, + + _clearMemoizedQueries: function() { + this._memoizedQueries = {}; + if (this._parentNode && this._parentNode !== this) { + this._parentNode._clearMemoizedQueries(); + } + }, + + _descendantRemoved: function(parent, child) { + if (this._parentNode && this._parentNode !== this) { + this._parentNode._descendantRemoved(parent, child); + } + }, + + _descendantAdded: function(parent, child) { + if (this._parentNode && this._parentNode !== this) { + this._parentNode._descendantAdded(parent, child); + } + }, + + _attrModified: function(name, value, oldValue) { + if (name == 'id' && this._attached) { + var doc = this._ownerDocument; + detachId(oldValue,this,doc); + attachId(value,this,doc); + } + + // Check for inline event handlers. + // We can't set these like other attributes then look it up in + // dispatchEvent() because that would create 2 'traditional' event handlers + // in the case where there's an inline event handler attribute, plus one + // set using element.on* in a script. + // + // @see http://www.w3.org/TR/2011/WD-html5-20110405/webappapis.html#event-handler-content-attributes + if ((name.length > 2) && (name[0] == 'o') && (name[1] == 'n')) { + if (value) { + var self = this; + // Check whether we're the window. This can happen because inline + // handlers on the body are proxied to the window. + var w = (typeof self.run !== 'undefined') ? self : self._ownerDocument.parentWindow; + self[name] = function (event) { + // The handler code probably refers to functions declared in the + // window context, so we need to call run(). + + // Use awesome hacks to get the correct `this` context for the + // inline event handler. This would only be necessary if we're an + // element, but for the sake of simplicity we also do it on window. + + // Also set event variable and support `return false`. + w.__tempContextForInlineEventHandler = self; + w.__tempEvent = event; + w.run("if ((function (event) {" + value + "}).call(" + + "window.__tempContextForInlineEventHandler, window.__tempEvent) === false) {" + + "window.__tempEvent.preventDefault()}"); + delete w.__tempContextForInlineEventHandler; + delete w.__tempEvent; + }; + } else { + this[name] = null; + } + } + }, + + /* returns Node */ + replaceChild : function(/* Node */ newChild, /* Node */ oldChild){ + this.insertBefore(newChild, oldChild); + return this.removeChild(oldChild); + }, //raises(DOMException); + + /* returns void */ + _attach : function() { + this._attached = true; + if (this.id) { + attachId(this.id,this,this._ownerDocument); + } + for (var i = 0, len = this._childNodes.length; i < len; i++) { + if (this._childNodes[i]._attach) { + this._childNodes[i]._attach(); + } + } + }, + /* returns void */ + _detach : function() { + var i, elms; + this._attached = false; + if (this.id) { + detachId(this.id,this,this._ownerDocument); + } + for (var i = 0, len = this._childNodes.length; i < len; i++) { + this._childNodes[i]._detach(); + } + }, + + /* returns Node */ + removeChild : function(/* Node */ oldChild){ + if (this._readonly === true) { + throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR); + } + + // TODO - if (!oldChild) then? + + // Use lastIndexOf so that removing all the children by + // going backwards through childNodes is fast + // (because of splice) + var oldChildIndex = this._childNodes.lastIndexOf(oldChild); + if (oldChildIndex == -1) { + throw new core.DOMException(NOT_FOUND_ERR); + } + + this._childNodes.splice(oldChildIndex, 1); + oldChild._parentNode = null; + this._modified(); + oldChild._detach(); + this._descendantRemoved(this, oldChild); + return oldChild; + }, // raises(DOMException); + + /* returns Node */ + appendChild : function(/* Node */ newChild) { + return this.insertBefore(newChild, null); + }, // raises(DOMException); + + /* returns boolean */ + hasChildNodes : function() { + return this._childNodes.length > 0; + }, + + /* returns Node */ + cloneNode : function(/* bool */ deep, fn) { + + var object = null; + switch (this.nodeType) { + + case this.ELEMENT_NODE: + object = attrCopy(this,this._ownerDocument.createElementNS(this.namespaceURI, this.nodeName), fn); + // Using this.nodeName isn't always exact because of uppercasing-related stuff + object._prefix = this._prefix; + object._localName = this._localName; + break; + + case this.TEXT_NODE: + object = attrCopy(this,this._ownerDocument.createTextNode(this.tagName)); + object.nodeValue = this.nodeValue; + break; + case this.ATTRIBUTE_NODE: + object = this._ownerDocument.createAttribute(this.name); + break; + break; + case this.PROCESSING_INSTRUCTION_NODE: + var pi = this._ownerDocument.createProcessingInstruction(this._target, + this._data); + object = attrCopy(this, pi); + break; + case this.COMMENT_NODE: + object = this._ownerDocument.createComment(this.tagName); + object.nodeValue = this.nodeValue; + break; + case this.DOCUMENT_NODE: + object = attrCopy(this, new this.constructor({ parsingMode: this._parsingMode })); + // TODO: clone the doctype? + break; + case this.DOCUMENT_TYPE_NODE: + object = new core.DocumentType(this._ownerDocument, this._name, this._publicId, this._systemId); + break; + case this.DOCUMENT_FRAGMENT_NODE: + object = this._ownerDocument.createDocumentFragment(); + break; + default: + throw new core.DOMException(NOT_FOUND_ERR); + break; + } + + if (typeof fn === "function") { + fn(this, object); + } + + if (deep || this.nodeType === ATTRIBUTE_NODE) { + var clone = null; + for (var i=0,len=this._childNodes.length;i<len;i++) + { + clone = this._childNodes[i].cloneNode(true); + if (clone.nodeType === ATTRIBUTE_NODE) { + object.setAttributeNode(clone); + } else { + var readonly = object._readonly; + object._readonly = false; + object.appendChild(clone); + object._readonly = readonly; + } + } + } + + return object; + }, + + /* returns void */ + normalize: function() { + var prevChild, child, attr,i; + + if (this._attributes && this._attributes.length) { + for (i=0;i<this._attributes.length;i++) + { + if (this._attributes[i]) { + attr = this._attributes[i].normalize(); + } + } + } + + for (i=0;i<this._childNodes.length;i++) + { + child = this._childNodes[i]; + + if (child.normalize) { + child.normalize(); + } + + // Level2/core clean off empty nodes + if (child.nodeValue === "") { + this.removeChild(child); + i--; + continue; + } + + if (i>0) { + prevChild = this._childNodes[i-1]; + + if (child.nodeType === TEXT_NODE && + prevChild.nodeType === TEXT_NODE) + { + + // remove the child and decrement i + prevChild.appendData(child.nodeValue); + + this.removeChild(child); + i--; + } + } + } + }, + toString: function() { + var id = ''; + if (this.id) { + id = '#' + this.id; + } + if (this.className) { + var classes = this.className.split(/\s+/); + for (var i = 0, len = classes.length; i < len; i++) { + id += '.' + classes[i]; + } + } + return '[ ' + this.tagName + id + ' ]'; + }, + raise: function(type, message, data) { + var text = type + ": " + message; + + if (data) { + if (data.exception) { + text = data.exception.stack; + } else { + text += ' - More:\n' + data; + } + } + + if (type === "error") { + if (!this.errors) { + this.errors = []; + } + // TODO: consider using actual `Error` objects or `DOMException`s even.. + var err = { + type : type, + message : message || "No message", + data : data || null + }; + + this.errors.push(err); + + if (this._ownerDocument && + this._ownerDocument.raise && + this !== this._ownerDocument) + { + this._ownerDocument.raise(type, message, data); + } + } + } +}; + + +core.NamedNodeMap = function NamedNodeMap(document) { + this._nodes = Object.create(null); + this._nsStore = {}; + this.length = 0; + this._ownerDocument = document; + this._readonly = false; +}; +core.NamedNodeMap.prototype = { + get readonly() { return this._readonly;}, + get ownerDocument() { this._ownerDocument;}, + + exists : function(name) { + return (this._nodes[name] || this._nodes[name] === null) ? true : false; + }, + + /* returns Node */ + getNamedItem: function(/* string */ name) { + return this._nodes[name] || null; + }, + + /* returns Node */ + setNamedItem: function(/* Node */ arg) { + + // readonly + if (this._readonly === true) { + throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR); + } + + // arg is from a different document + if (arg && arg._ownerDocument !== this._ownerDocument) { + throw new core.DOMException(WRONG_DOCUMENT_ERR); + } + + // if this argument is already in use.. + if (arg && arg._ownerElement) { + throw new core.DOMException(INUSE_ATTRIBUTE_ERR); + } + + var name = arg.name || arg.tagName; + var ret = this._nodes[name]; + if (!ret) { + this.length++; + ret = null; + } + this._nodes[name] = arg; + + // Avoid overwriting prototype methods etc.: + if (this.hasOwnProperty(name) || !(name in this)) { + this[name] = arg; + } + return ret; + }, // raises: function(DOMException) {}, + + /* returns Node */ + removeNamedItem: function(/* string */ name) { + + // readonly + if (this._readonly === true) { + throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR); + } + + if (!this._nodes[name]) { + throw new core.DOMException(NOT_FOUND_ERR); + } + + var prev = this._nodes[name] || null; + delete this._nodes[name]; + delete this[name]; + + this.length--; + return prev; + }, // raises: function(DOMException) {}, + + /* returns Node */ + item: function(/* int */ index) { + var current = 0; + for (var member in this._nodes) { + if (current === index && this._nodes[member]) { + return this._nodes[member]; + } + current++; + } + return null; + } +}; + +// +// For historical reasons, AttributeList objects must allow accessing +// attributes as if the object were an associative array. For +// instance, if `attributes` is an AttributeList object then +// `attributes.x` should evaluate to the attribute named `x` (which is +// not in any namespace). The AttributeList class uses the dollar +// symbol ($) to reduce the possibility of a clash between its field +// names and possible attribute names. For instance, if the method +// currently named `$set` were instead named `set` then it would not +// be possible to access an attribute named `set` through +// `attributes.set`. The dollar symbol is not valid in attribute names +// so `$set` cannot clash. +// +// Some fields do not get the $ because: +// +// * They are part of the API (e.g. `setNamedItem`, `length`), so they +// must be visible under a specific name. +// +// * Jsdom's code which traverses the DOM tree expects regularly named +// fields (e.g. `_parentNode`). +// +function AttributeList(document, parentNode) { + this._ownerDocument = document; + this._parentNode = parentNode; + this._readonly = false; + this._$ns_to_attrs = Object.create(null); + this._$name_to_attrs = Object.create(null); + this.length = 0; +} + +AttributeList.prototype = { + _$reserved: [], // Initialized later + + + // + // Code internal to jsdom and which manipulates an AttributeList + // object should use the following methods rather than the methods + // that provide the NamedNodeMap interface. + // + + // This method *ignores* namespaces. This is *not* the same thing as + // requesting an attribute with a null namespace. + $getNoNS: function (name) { + var attrs = this._$name_to_attrs[name]; + if (!attrs) { + return null; + } + + return attrs[0] || null; + }, + + $getNode: function (namespace, localName) { + var attrs = this._$ns_to_attrs[namespace]; + if (!attrs) { + return null; + } + + var ret = attrs[localName]; + if (!ret) { + return null; + } + + return ret; + }, + + // This method *ignores* namespaces. This is *not* the same thing as + // requesting an attribute with a null namespace. + $setNoNS: function (name, value, dontValidate) { + var attr = this.$getNoNS(name); + if (!attr) { + this.$set(name, value, undefined, undefined, undefined, dontValidate); + return; + } + + var prev_val = attr.value; + attr.value = value; + + this._parentNode._attrModified(attr.name, attr.value, prev_val); + this._parentNode._modified(); + }, + + $set: function (localName, value, name, prefix, namespace, dontValidate) { + if (this._readonly) { + throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR); + } + + if (name === undefined) { + name = localName; + } + + if (prefix === undefined) { + prefix = null; + } + + if (namespace === undefined) { + namespace = null; + } + + var prev_attr = this.$getNode(namespace, localName); + var attr; + + var prev_val = null; + if (prev_attr) { + prev_val = prev_attr.value; + prev_attr._prefix = prefix; + prev_attr.value = value; + attr = prev_attr; + + this._parentNode._attrModified(attr.name, attr.value, prev_val); + this._parentNode._modified(); + } + else { + var method = dontValidate ? '_createAttributeNoNameValidation' : 'createAttribute'; + attr = this._ownerDocument[method](name); + attr._ownerElement = this._parentNode; + attr.value = value; + attr._namespaceURI = namespace; + attr._prefix = prefix; + attr._localName = localName; + attr._parentNode = this._parentNode; + this.$setNode(attr); + // $setNode calls the parent node methods. + } + }, + + $setNode: function (attr) { + if (this._readonly) { + throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR); + } + + if (attr.nodeType !== ATTRIBUTE_NODE) { + throw new core.DOMException(HIERARCHY_REQUEST_ERR); + } + + if (attr._ownerDocument !== this._ownerDocument) { + throw new core.DOMException(WRONG_DOCUMENT_ERR); + } + + if (attr._parentNode && attr._parentNode !== this._parentNode) { + throw new core.DOMException(INUSE_ATTRIBUTE_ERR); + } + + var localName = attr._localName; + var name = attr.name; + var prefix = attr._prefix; + var namespace = attr._namespaceURI; + + if (name === undefined) { + name = localName; + } + + if (prefix === undefined) { + prefix = null; + } + + if (namespace === undefined) { + namespace = null; + } + + var prev_attr = this.$getNode(namespace, localName); + + var prev_val = null; + if (prev_attr) { + prev_val = prev_attr.value; + // Remove the old attribute + this._$onlyRemoveNode(prev_attr); + } + + attr._parentNode = this._parentNode; + attr._ownerElement = this._parentNode; + + var attrs = this._$ns_to_attrs[namespace]; + if (!attrs) { + attrs = this._$ns_to_attrs[namespace] = Object.create(null); + } + attrs[localName] = attr; + + attrs = this._$name_to_attrs[name]; + if (!attrs) { + attrs = this._$name_to_attrs[name] = [attr]; + } + else { + attrs.push(attr); + } + + // Only attributes in the null namespace can be set this way. + if (namespace === null) { + // Make the node a field on this object but ONLY if it does not + // clash with the reserved names. + if (this._$reserved.indexOf(name) === -1) { + this[name] = attr; + } + } + + this[this.length] = attr; + this.length++; + + this._parentNode._attrModified(attr.name, attr.value, prev_val); + this._parentNode._modified(); + + return prev_attr; + }, + + // This method *ignores* namespaces. This is *not* the same thing as + // requesting an attribute with a null namespace. + $removeNoNS: function (name) { + var attr = this.$getNoNS(name); + return attr ? this.$removeNode(attr) : null; + }, + + $remove: function (namespace, localName) { + var attr = this.$getNode(namespace, localName); + return attr ? this.$removeNode(attr) : null; + }, + + /* Only removes the node, and does not add a default value. */ + _$onlyRemoveNode: function (attr) { + var namespace = attr._namespaceURI; + var localName = attr._localName; + + var attrs = this._$ns_to_attrs[namespace]; + if (!attrs) { + return null; + } + + var found_attr = attrs[localName]; + if (found_attr !== attr) { + return null; + } + + if (this._readonly) { + throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR); + } + + attr._ownerElement = null; + attr._parentNode = null; + delete attrs[localName]; + + attrs = this._$name_to_attrs[attr.name]; + attrs.splice(attrs.indexOf(attr), 1); + + var ix = Array.prototype.indexOf.call(this, attr); + // Splice also modifies length. + Array.prototype.splice.call(this, ix, 1); + + if (this[attr.name] === attr) { + delete this[attr.name]; + } + + this._parentNode._attrModified(attr.name); + this._parentNode._modified(); + + return attr; + }, + + $removeNode: function (attr) { + if (!this._$onlyRemoveNode(attr)) { + return null; + } + return attr; + }, + + // Although http://dom.spec.whatwg.org/#concept-element-attribute + // does not specify that the attributes field on an Element should + // support NamedNodeMap, in practice browsers still support this + // interface so we should support it for compatibility. + + getNamedItem: function (name) { + return this.getNamedItemNS(null, name); + }, + removeNamedItem: function (name) { + return this.removeNamedItemNS(null, name); + }, + item: function (i) { + return this[i]; + }, + getNamedItemNS: function (namespaceURI, localName) { + if (namespaceURI === "") { + namespaceURI = null; + } + + return this.$getNode(namespaceURI, localName); + }, + removeNamedItemNS: function (namespaceURI, localName) { + var ret = this.$remove(namespaceURI, localName); + + if (ret === null) { + throw new core.DOMException(NOT_FOUND_ERR); + } + + return ret; + } +}; + +// Alias these methods. +AttributeList.prototype.setNamedItem = AttributeList.prototype.$setNode; +AttributeList.prototype.setNamedItemNS = AttributeList.prototype.$setNode; + +(function () { + // Construct the list of reserved attribute names from a temporarily + // created AttributeList and from the chain of prototypes. We need + // this because JavaScript code running an a browser expects to be + // able to do el.attributes.x to get the value of the attribute "x" + // on an element. Unfortunately, JavaScript *currently* does not + // allow us to elegantly provide such functionality without risking + // a clash with the fields and methods set on the AttributeList + // object. Hence we need a list of reserved field names. + + var reserved = Object.keys(new AttributeList()); + var prototype = AttributeList.prototype; + while (prototype) { + reserved = reserved.concat(Object.getOwnPropertyNames(prototype)); + prototype = Object.getPrototypeOf(prototype); + } + AttributeList.prototype._$reserved = reserved; +})(); + +core.AttributeList = AttributeList; + +core.Element = function Element(document, localName) { + core.Node.call(this, document); + this._namespaceURI = null; + this._prefix = null; + this._localName = localName; +}; + +inheritFrom(core.Node, core.Element, { + get namespaceURI() { + return this._namespaceURI; + }, + get prefix() { + return this._prefix; + }, + get localName() { + return this._localName; + }, + get tagName() { + var qualifiedName = this._prefix !== null ? this._prefix + ":" + this._localName : this._localName; + if (this.namespaceURI === "http://www.w3.org/1999/xhtml" && this._ownerDocument._parsingMode === "html") { + qualifiedName = qualifiedName.toUpperCase(); + } + return qualifiedName; + }, + + get id() { + var idAttr = this.getAttribute("id"); + if (idAttr === null) { + return ""; + } + return idAttr; + }, + + nodeType : ELEMENT_NODE, + get attributes() { + return this._attributes; + }, + + get sourceIndex() { + /* + * According to QuirksMode: + * Get the sourceIndex of element x. This is also the index number for + * the element in the document.getElementsByTagName('*') array. + * http://www.quirksmode.org/dom/w3c_core.html#t77 + */ + var items = this.ownerDocument.getElementsByTagName('*'), + len = items.length; + + for (var i = 0; i < len; i++) { + if (items[i] === this) { + return i; + } + } + }, + + get outerHTML() { + return domToHtml(this, true); + }, + + get innerHTML() { + var tagName = this.tagName; + if (tagName === 'SCRIPT' || tagName === 'STYLE') { + var type = this.getAttribute('type'); + if (!type || /^text\//i.test(type) || /\/javascript$/i.test(type)) { + return domToHtml(this._childNodes, true, true); + } + } + + // In case of <template> we should pass it's content fragment as a serialization root if we have one + if(tagName === 'TEMPLATE' && + this._namespaceURI === 'http://www.w3.org/1999/xhtml' && + this._childNodes[0] && this._childNodes[0]._templateContent) { + return domToHtml(this._childNodes[0]._childNodes, true); + } + + return domToHtml(this._childNodes, true); + }, + + set innerHTML(html) { + setInnerHTML(this.ownerDocument, this, html); + }, + + scrollTop: 0, + scrollLeft: 0, + + hasAttributes: function () { + return this._attributes.length > 0; + }, + + /* returns Attr */ + setAttributeNode: function(/* Attr */ newAttr) { + var prevNode = this._attributes.$getNode(null, newAttr.name); + if (prevNode) { + prevNode._ownerElement = null; + } + + newAttr._ownerElement = this; + this._attributes.$setNode(newAttr); + + return (prevNode && prevNode.specified) ? prevNode : null; + }, // raises: function(DOMException) {}, + + /* returns Attr */ + removeAttributeNode: function(/* Attr */ oldAttr) { + var ret = this._attributes.$removeNode(oldAttr); + + if (ret !== null) { + return ret; + } + + throw new core.DOMException(NOT_FOUND_ERR); + }, //raises: function(DOMException) {}, + + /* returns NodeList */ + getElementsByTagName: memoizeQuery(function(/* string */ name) { + name = name.toLowerCase(); + + function filterByTagName(child) { + if (child.nodeName && child.nodeType === ELEMENT_NODE) { + return name === "*" || (child.nodeName.toLowerCase() === name); + } + + return false; + } + return new core.NodeList(this._ownerDocument || this, core.mapper(this, filterByTagName, true)); + }), + + getElementsByClassName: function (className) { + + function filterByClassName(child) { + if (!child) { + return false; + } + + var classString = child.className; + if (classString) { + var s = classString.split(" "); + for (var i = 0; i < s.length; i++) { + if (s[i] === className) { + return true; + } + } + } + return false; + } + + return new core.NodeList(this.ownerDocument || this, core.mapper(this, filterByClassName)); + } +}); + +core.DocumentFragment = function DocumentFragment(document) { + core.Node.call(this, document); +}; +inheritFrom(core.Node, core.DocumentFragment, { + nodeType : DOCUMENT_FRAGMENT_NODE +}); + +core.Document = function Document(options) { + if (!options || !options.parsingMode || (options.parsingMode !== "html" && options.parsingMode !== "xml")) { + throw new Error("options must exist and contain a parsingMode of html or xml"); + } + + core.Node.call(this, "#document"); + this._parsingMode = options.parsingMode; + this._implementation = new core.DOMImplementation(this); + this._documentElement = null; + this._ids = Object.create(null); + this._attached = true; + this._ownerDocument = this; + this._readonly = false; + + this._contentType = options.contentType; + if (this._contentType === undefined) { + this._contentType = this._parsingMode === "xml" ? "application/xml" : "text/html"; + } + + this._URL = options.url; + if (this._URL === undefined) { + this._URL = "about:blank"; + } + this._location = new Location(this._URL, this); +}; + + +var tagRegEx = /[^\w:\d_\.-]+/i; +var entRegEx = /[^\w\d_\-&;]+/; +var invalidAttrRegEx = /[\s"'>/=\u0000-\u001A]/; + +inheritFrom(core.Node, core.Document, { + nodeType : DOCUMENT_NODE, + _elementBuilders : { }, + _defaultElementBuilder: function(document, tagName) { + return new core.Element(document, tagName); + }, + get contentType() { return this._contentType;}, + get compatMode() { return (this._parsingMode === "xml" || this.doctype) ? "CSS1Compat" : "BackCompat"; }, + get characterSet() { return "UTF-8"; }, + get inputEncoding() { return "UTF-8"; }, + get doctype() { + for (var i = 0; i < this._childNodes.length; ++i) { + if (this._childNodes[i].nodeType === DOCUMENT_TYPE_NODE) { + return this._childNodes[i]; + } + } + return null; + }, + get URL() { + return this._URL; + }, + get documentURI() { + return this._URL; + }, + get location() { + return this._location; + }, + get documentElement() { + if (this._documentElement) { + return this._documentElement; + } else { + for (var i = 0; i < this._childNodes.length; ++i) { + if (this._childNodes[i].nodeType === ELEMENT_NODE) { + this._documentElement = this._childNodes[i]; + return this._documentElement; + } + } + return null; + } + }, + + get implementation() { return this._implementation;}, + set implementation(implementation) { this._implementation = implementation;}, + get ownerDocument() { return null;}, + get readonly() { return this._readonly;}, + + set parentWindow(window) { + // Contextify does not support getters and setters, so we have to set them + // on the original object instead. + window._frame = function (name, frame) { + if (typeof frame === 'undefined') { + delete window[name]; + } else { + defineGetter(window, name, function () { return frame.contentWindow; }); + } + }; + this._parentWindow = window.getGlobal(); + }, + + get defaultView() { + return this.parentWindow; + }, + + toString: function () { + return '[object HTMLDocument]'; + }, + + _createElementNoTagNameValidation: function (tagName) { + var element = (this._elementBuilders[tagName.toLowerCase()] || this._defaultElementBuilder)(this, tagName); + element._namespaceURI = "http://www.w3.org/1999/xhtml"; + return element; + }, + + createElement: function (localName) { + localName = String(localName); + validateName(localName, core); + if (this._parsingMode === "html") { + localName = localName.toLowerCase(); + } + + return this._createElementNoTagNameValidation(localName); + }, + + /* returns DocumentFragment */ + createDocumentFragment: function() { + return new core.DocumentFragment(this); + }, + + /* returns Attr */ + createAttribute: function (localName) { + localName = String(localName); + validateName(localName, core); + + return this._createAttributeNoNameValidation(localName); + }, // raises: function(DOMException) {}, + + _createAttributeNoNameValidation: function (localName) { + return new core.Attr(this, localName, ""); + }, + + appendChild : function(/* Node */ arg) { + if (this.documentElement && arg.nodeType == ELEMENT_NODE) { + throw new core.DOMException(HIERARCHY_REQUEST_ERR); + } + return core.Node.prototype.appendChild.call(this, arg); + }, + + removeChild : function(/* Node */ arg) { + var ret = core.Node.prototype.removeChild.call(this, arg); + if (arg == this._documentElement) { + this._documentElement = null;// force a recalculation + } + return ret; + }, + + /* returns NodeList */ + getElementsByTagName: memoizeQuery(function(/* string */ name) { + function filterByTagName(child) { + if (child.nodeName && child.nodeType === ELEMENT_NODE) + { + if (name === "*") { + return true; + + // case insensitivity for html + } else if (child._ownerDocument && child._ownerDocument._doctype && + //child._ownerDocument._doctype.name === "html" && + child.nodeName.toLowerCase() === name.toLowerCase()) + { + return true; + } else if (child.nodeName.toLowerCase() === name.toLowerCase()) { + return true; + } + } + return false; + } + return new core.NodeList(this.documentElement || this, core.mapper(this, filterByTagName, true)); + }), + + getElementsByClassName: function (className) { + + function filterByClassName(child) { + if (!child) { + return false; + } + + var classString = child.className; + if (classString) { + var s = classString.split(" "); + for (var i = 0; i < s.length; i++) { + if (s[i] === className) { + return true; + } + } + } + return false; + } + + return new core.NodeList(this.ownerDocument || this, core.mapper(this, filterByClassName)); + }, + + write: function (text) { + if (this._writeAfterElement) { + // If called from an script element directly (during the first tick), + // the new elements are inserted right after that element. + var tempDiv = this.createElement('div'); + setInnerHTML(this, tempDiv, text); + + var child = tempDiv.firstChild; + var previous = this._writeAfterElement; + var parent = this._writeAfterElement.parentNode; + + while (child) { + var node = child; + child = child.nextSibling; + parent.insertBefore(node, previous.nextSibling); + previous = node; + } + } else if (this.readyState === "loading") { + // During page loading, document.write appends to the current element + // Find the last child that has been added to the document. + var node = this; + while (node.lastChild && node.lastChild.nodeType === this.ELEMENT_NODE) { + node = node.lastChild; + } + setInnerHTML(this, node, text || "<html><head></head><body></body></html>"); + } else if (text) { + setInnerHTML(this, this, text); + } + } +}); + +core.Attr = function Attr(document, name, value) { + core.Node.call(this, document); + this._valueForAttrModified = value; + this._name = name; + this._ownerElement = null; + this._namespaceURI = null; + this._localName = name; + this._prefix = null; +}; +inheritFrom(core.Node, core.Attr, { + nodeType : ATTRIBUTE_NODE, + get namespaceURI() { + return this._namespaceURI; + }, + get prefix() { + return this._prefix; + }, + get localName() { + return this._localName; + }, + get name() { + return this._name; + }, + get ownerElement() { + return this._ownerElement; + }, + get nodeValue() { + var val = ''; + for (var i=0,len=this._childNodes.length;i<len;i++) { + var child = this._childNodes[i]; + val += child.nodeValue; + } + return val; + }, + set nodeValue(value) { + // readonly + if (this._readonly) { + throw new core.DOMException(NO_MODIFICATION_ALLOWED_ERR); + } + + this._childNodes.length = 1; + this._childNodes[0] = this._ownerDocument.createTextNode(value); + this._modified(); + var prev = this._valueForAttrModified; + this._nodeValue = value; + if (this._ownerElement) { + this._ownerElement._attrModified(this._name, value, prev); + } + }, + get specified() { + return true; + }, + get value() { + return this.nodeValue; + }, + set value(value) { + this.nodeValue = value; + }, + get parentNode() { return null;}, + + insertBefore : function(/* Node */ newChild, /* Node*/ refChild){ + if (newChild.nodeType === CDATA_SECTION_NODE || + newChild.nodeType === ELEMENT_NODE) + { + throw new core.DOMException(HIERARCHY_REQUEST_ERR); + } + + return core.Node.prototype.insertBefore.call(this, newChild, refChild); + }, + + appendChild : function(/* Node */ arg) { + + if (arg.nodeType === CDATA_SECTION_NODE || + arg.nodeType === ELEMENT_NODE) + { + throw new core.DOMException(HIERARCHY_REQUEST_ERR); + } + + return core.Node.prototype.appendChild.call(this, arg); + } + +}); diff --git a/node_modules/jsdom/lib/jsdom/level2/core.js b/node_modules/jsdom/lib/jsdom/level2/core.js new file mode 100644 index 0000000..6a52baf --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level2/core.js @@ -0,0 +1,306 @@ +var core = require("../level1/core"); +var defineGetter = require('../utils').defineGetter; +var defineSetter = require('../utils').defineSetter; +var memoizeQuery = require('../utils').memoizeQuery; +var validateQname = require('../living/helpers/validate-names').qname; +var validateAndExtract = require('../living/helpers/validate-names').validateAndExtract; + +core.NamedNodeMap.prototype.getNamedItemNS = function(/* string */ namespaceURI, + /* string */ localName) +{ + if (this._nsStore[namespaceURI] && this._nsStore[namespaceURI][localName]) { + return this._nsStore[namespaceURI][localName]; + } + return null; +}; + +core.NamedNodeMap.prototype.setNamedItemNS = function(/* Node */ arg) +{ + if (this._readonly) { + throw new core.DOMException(core.NO_MODIFICATION_ALLOWED_ERR); + } + + if (this._ownerDocument !== arg.ownerDocument) { + throw new core.DOMException(core.WRONG_DOCUMENT_ERR); + } + + if (arg._ownerElement) { + throw new core.DOMException(core.INUSE_ATTRIBUTE_ERR); + } + + // readonly + if (this._readonly === true) { + throw new core.DOMException(core.NO_MODIFICATION_ALLOWED_ERR); + } + + + if (!this._nsStore[arg.namespaceURI]) { + this._nsStore[arg.namespaceURI] = {}; + } + var existing = null; + if (this._nsStore[arg.namespaceURI][arg.localName]) { + var existing = this._nsStore[arg.namespaceURI][arg.localName]; + } + + this._nsStore[arg.namespaceURI][arg.localName] = arg; + + arg._ownerDocument = this._ownerDocument; + + return this.setNamedItem(arg); +}; + +core.NamedNodeMap.prototype.removeNamedItemNS = function(/*string */ namespaceURI, + /* string */ localName) +{ + + if (this.readonly) { + throw new core.DOMException(core.NO_MODIFICATION_ALLOWED_ERR); + } + + + var parent = this._parentNode, + found = null, + defaults, + clone, + defaultEl, + defaultAttr; + + if (this._nsStore[namespaceURI] && + this._nsStore[namespaceURI][localName]) + { + found = this._nsStore[namespaceURI][localName]; + this.removeNamedItem(found.qualifiedName); + delete this._nsStore[namespaceURI][localName]; + } + + if (!found) { + throw new core.DOMException(core.NOT_FOUND_ERR); + } + + return found; +}; + +core.NamedNodeMap.prototype._map = function(fn) { + var ret = [], l = this.length, i = 0, node; + for(i; i<l; i++) { + node = this.item(i); + if (fn && fn(node)) { + ret.push(node); + } + } + return ret; +}; + +core.Element.prototype.getAttribute = function(/* string */ name) +{ + var attr = this.getAttributeNode(name); + return attr && attr.value; +}; + +core.Element.prototype.getAttributeNode = function(/* string */ name) +{ + return this._attributes.$getNoNS(name); +}; + +core.Element.prototype.removeAttribute = function(/* string */ name) +{ + return this._attributes.$removeNoNS(name); +}; + +core.Element.prototype.getAttributeNS = function(/* string */ namespaceURI, + /* string */ localName) +{ + if (namespaceURI === "") { + namespaceURI = null; + } + + var attr = this._attributes.$getNode(namespaceURI, localName); + return attr && attr.value; +}; + +core.Element.prototype.setAttribute = function(/* string */ name, + /* string */ value) +{ + this._attributes.$setNoNS(name, value); +}; + +core.Element.prototype._setAttributeNoValidation = function (name, value) { + this._attributes.$setNoNS(name, value, true); +}; + +core.Element.prototype.setAttributeNS = function (namespace, name, value) { + namespace = namespace !== null ? String(namespace) : namespace; + name = String(name); + value = String(value); + + var extracted = validateAndExtract(namespace, name, core); + + this._attributes.$set(extracted.localName, value, extracted.qualifiedName, extracted.prefix, extracted.namespace); +}; + +core.Element.prototype.removeAttributeNS = function(/* string */ namespaceURI, + /* string */ localName) +{ + if (namespaceURI === "") { + namespaceURI = null; + } + + this._attributes.$remove(namespaceURI, localName); +}; + +core.Element.prototype.getAttributeNodeNS = function(/* string */ namespaceURI, + /* string */ localName) +{ + if (namespaceURI === "") { + namespaceURI = null; + } + + return this._attributes.$getNode(namespaceURI, localName); +}; + +core.Element.prototype.setAttributeNodeNS = function(/* Attr */ newAttr) +{ + if (newAttr.ownerElement) { + throw new core.DOMException(core.INUSE_ATTRIBUTE_ERR); + } + + return this._attributes.$setNode(newAttr); +}; + +core.Element.prototype.getElementsByTagNameNS = memoizeQuery(function(/* String */ namespaceURI, + /* String */ localName) +{ + var nsPrefixCache = {}; + + function filterByTagName(child) { + var localMatch = child.localName === localName, + nsMatch = child.namespaceURI === namespaceURI; + + if ((localMatch || localName === "*") && + (nsMatch || namespaceURI === "*")) + { + if (child.nodeType === child.ELEMENT_NODE) { + return true; + } + } + return false; + } + + return new core.NodeList(this.ownerDocument || this, + core.mapper(this, filterByTagName)); +}); + +core.Element.prototype.hasAttribute = function(/* string */name) +{ + if (!this._attributes) { + return false; + } + + // Note: you might think you only need the latter condition. However, it makes a test case fail. + // HOWEVER, that test case is for a XML DTD feature called "default attributes" that never was implemented by + // browsers, so when we remove default attributes, we should be able to fix this code too. + return !!this._attributes.$getNoNS(name) || !!this._attributes.$getNoNS(name.toLowerCase()); +}; + +core.Element.prototype.hasAttributeNS = function(/* string */namespaceURI, + /* string */localName) +{ + if (namespaceURI === "") { + namespaceURI = null; + } + + return (this._attributes.getNamedItemNS(namespaceURI, localName) || + this.hasAttribute(localName)); +}; + +core.Document.prototype.importNode = function(/* Node */ importedNode, + /* bool */ deep) +{ + if (importedNode && importedNode.nodeType) { + if (importedNode.nodeType === this.DOCUMENT_NODE || + importedNode.nodeType === this.DOCUMENT_TYPE_NODE) { + throw new core.DOMException(core.NOT_SUPPORTED_ERR); + } + } + + var self = this, + newNode = importedNode.cloneNode(deep, function(a, b) { + b._prefix = a._prefix; + b._namespaceURI = a._namespaceURI; + b._localName = a._localName; + }); + + function lastChance(el) { + var attr, defaultEl, i, len; + + el._ownerDocument = self; + if (el.id) { + if (!self._ids) {self._ids = {};} + if (!self._ids[el.id]) {self._ids[el.id] = [];} + self._ids[el.id].push(el); + } + if (el._attributes) { + var drop = []; + el._attributes._ownerDocument = self; + for (i=0,len=el._attributes.length; i < len; i++) { + attr = el._attributes[i]; + attr._ownerDocument = self; + } + + // Remove obsolete default nodes. + for(i = 0; i < drop.length; ++i) { + el._attributes.$removeNode(drop[i]); + } + + } + } + + if (deep) { + core.visitTree(newNode, lastChance); + } + else { + lastChance(newNode); + } + + return newNode; +}; + +core.Document.prototype.createElementNS = function (namespace, qualifiedName) { + namespace = namespace !== null ? String(namespace) : namespace; + qualifiedName = String(qualifiedName); + + var extracted = validateAndExtract(namespace, qualifiedName, core); + var element = this.createElement(extracted.localName); + element._namespaceURI = extracted.namespace; + element._prefix = extracted.prefix; + + return element; +}; + +core.Document.prototype.createAttributeNS = function (namespace, qualifiedName) { + namespace = namespace !== null ? String(namespace) : namespace; + qualifiedName = String(qualifiedName); + + var extracted = validateAndExtract(namespace, qualifiedName, core); + attribute = this.createAttribute(extracted.qualifiedName); + + attribute._namespaceURI = extracted.namespace; + attribute._prefix = extracted.prefix; + attribute._localName = extracted.localName; + attribute._name = extracted.qualifiedName; + + return attribute; +}; + +core.Document.prototype.getElementsByTagNameNS = function(/* String */ namespaceURI, + /* String */ localName) +{ + return core.Element.prototype.getElementsByTagNameNS.call(this, + namespaceURI, + localName); +}; + +core.Document.prototype.getElementById = function(id) { + // return the first element + return (this._ids && this._ids[id] && this._ids[id].length > 0 ? this._ids[id][0] : null); +}; diff --git a/node_modules/jsdom/lib/jsdom/level2/events.js b/node_modules/jsdom/lib/jsdom/level2/events.js new file mode 100644 index 0000000..2b19033 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level2/events.js @@ -0,0 +1,441 @@ +/* DOM Level2 Events implemented as described here: + * + * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html + * + */ +var core = require("../level1/core"), + utils = require("../utils"), + defineGetter = utils.defineGetter, + defineSetter = utils.defineSetter, + inheritFrom = utils.inheritFrom; + +core.EventException = function() { + if (arguments.length > 0) { + this._code = arguments[0]; + } else { + this._code = 0; + } + if (arguments.length > 1) { + this._message = arguments[1]; + } else { + this._message = "Unspecified event type"; + } + Error.call(this, this._message); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, core.EventException); + } +}; +inheritFrom(Error, core.EventException, { + UNSPECIFIED_EVENT_TYPE_ERR : 0, + get code() { return this._code;} +}); + +core.Event = function(eventType) { + this._eventType = eventType; + this._type = null; + this._bubbles = null; + this._cancelable = null; + this._target = null; + this._currentTarget = null; + this._eventPhase = 0; + this._timeStamp = null; + this._preventDefault = false; + this._stopPropagation = false; +}; +core.Event.prototype = { + initEvent: function(type, bubbles, cancelable) { + this._type = type; + this._bubbles = bubbles; + this._cancelable = cancelable; + }, + preventDefault: function() { + if (this._cancelable) { + this._preventDefault = true; + } + }, + stopPropagation: function() { + this._stopPropagation = true; + }, + NONE : 0, + CAPTURING_PHASE : 1, + AT_TARGET : 2, + BUBBLING_PHASE : 3, + get eventType() { return this._eventType; }, + get type() { return this._type; }, + get bubbles() { return this._bubbles; }, + get cancelable() { return this._cancelable; }, + get target() { return this._target; }, + get currentTarget() { return this._currentTarget; }, + get eventPhase() { return this._eventPhase; }, + get timeStamp() { return this._timeStamp; } +}; + + +core.UIEvent = function(eventType) { + core.Event.call(this, eventType); + this.view = null; + this.detail = null; +}; +inheritFrom(core.Event, core.UIEvent, { + initUIEvent: function(type, bubbles, cancelable, view, detail) { + this.initEvent(type, bubbles, cancelable); + this.view = view; + this.detail = detail; + }, +}); + + +core.MouseEvent = function(eventType) { + core.UIEvent.call(this, eventType); + this.screenX = null; + this.screenY = null; + this.clientX = null; + this.clientY = null; + this.ctrlKey = null; + this.shiftKey = null; + this.altKey = null; + this.metaKey = null; + this.button = null; + this.relatedTarget = null; +}; +inheritFrom(core.UIEvent, core.MouseEvent, { + initMouseEvent: function(type, + bubbles, + cancelable, + view, + detail, + screenX, + screenY, + clientX, + clientY, + ctrlKey, + altKey, + shiftKey, + metaKey, + button, + relatedTarget) { + this.initUIEvent(type, bubbles, cancelable, view, detail); + this.screenX = screenX + this.screenY = screenY + this.clientX = clientX + this.clientY = clientY + this.ctrlKey = ctrlKey + this.shiftKey = shiftKey + this.altKey = altKey + this.metaKey = metaKey + this.button = button + this.relatedTarget = relatedTarget + } +}); + + +core.MutationEvent = function(eventType) { + core.Event.call(this, eventType); + this.relatedNode = null; + this.prevValue = null; + this.newValue = null; + this.attrName = null; + this.attrChange = null; +}; +inheritFrom(core.Event, core.MutationEvent, { + initMutationEvent: function(type, + bubbles, + cancelable, + relatedNode, + prevValue, + newValue, + attrName, + attrChange) { + this.initEvent(type, bubbles, cancelable); + this.relatedNode = relatedNode; + this.prevValue = prevValue; + this.newValue = newValue; + this.attrName = attrName; + this.attrChange = attrChange; + }, + MODIFICATION : 1, + ADDITION : 2, + REMOVAL : 3 +}); + +core.EventTarget = function() {}; + +function getListeners(target, type, capturing) { + var listeners = target._listeners + && target._listeners[type] + && target._listeners[type][capturing] || []; + if (!capturing) { + var traditionalHandler = target['on' + type]; + if (traditionalHandler) { + var implementation = (target._ownerDocument ? target._ownerDocument.implementation + : target.document.implementation); + + if (implementation._hasFeature('ProcessExternalResources', 'script')) { + if (listeners.indexOf(traditionalHandler) < 0) { + listeners.push(traditionalHandler); + } + } + } + } + return listeners; +} + +function dispatchPhase(event, iterator) { + var target = iterator(); + + while (target && !event._stopPropagation) { + if (event._eventPhase === event.CAPTURING_PHASE || event._eventPhase === event.AT_TARGET) { + callListeners(event, target, getListeners(target, event._type, true)); + } + if (event._eventPhase === event.AT_TARGET || event._eventPhase === event.BUBBLING_PHASE) { + callListeners(event, target, getListeners(target, event._type, false)); + } + target = iterator(); + } +} + +function callListeners(event, target, listeners) { + var currentListener = listeners.length; + while (currentListener--) { + event._currentTarget = target; + try { + listeners[currentListener].call(target, event); + } catch (e) { + target.raise( + 'error', "Dispatching event '" + event._type + "' failed", + {error: e, event: event} + ); + } + } +} + +function forwardIterator(list) { + var i = 0, len = list.length; + return function iterator() { return i < len ? list[i++] : null }; +} + +function backwardIterator(list) { + var i = list.length; + return function iterator() { return i >=0 ? list[--i] : null }; +} + +function singleIterator(obj) { + var i = 1; + return function iterator() { return i-- ? obj : null }; +} + +core.EventTarget.prototype = { + addEventListener: function(type, listener, capturing) { + this._listeners = this._listeners || {}; + var listeners = this._listeners[type] || {}; + capturing = (capturing === true); + var capturingListeners = listeners[capturing] || []; + for (var i=0; i < capturingListeners.length; i++) { + if (capturingListeners[i] === listener) { + return; + } + } + capturingListeners.push(listener); + listeners[capturing] = capturingListeners; + this._listeners[type] = listeners; + }, + + removeEventListener: function(type, listener, capturing) { + var listeners = this._listeners && this._listeners[type]; + if (!listeners) return; + var capturingListeners = listeners[(capturing === true)]; + if (!capturingListeners) return; + for (var i=0; i < capturingListeners.length; i++) { + if (capturingListeners[i] === listener) { + capturingListeners.splice(i, 1); + return; + } + } + }, + + dispatchEvent: function(event) { + if (event == null) { + throw new core.EventException(0, "Null event"); + } + if (event._type == null || event._type == "") { + throw new core.EventException(0, "Uninitialized event"); + } + + var targetList = []; + + event._target = this; + + //per the spec we gather the list of targets first to ensure + //against dom modifications during actual event dispatch + var target = this, + targetParent = target._parentNode; + while (targetParent) { + targetList.push(targetParent); + target = targetParent; + targetParent = target._parentNode; + } + targetParent = target._parentWindow; + if (targetParent) { + targetList.push(targetParent); + } + + var iterator = backwardIterator(targetList); + + event._eventPhase = event.CAPTURING_PHASE; + dispatchPhase(event, iterator); + + iterator = singleIterator(event._target); + event._eventPhase = event.AT_TARGET; + dispatchPhase(event, iterator); + + if (event._bubbles) { + iterator = forwardIterator(targetList); + event._eventPhase = event.BUBBLING_PHASE; + dispatchPhase(event, iterator); + } + + event._currentTarget = null; + event._eventPhase = event.NONE; + + return !event._preventDefault; + } + +}; + +// Reinherit class heirarchy with EventTarget at its root +inheritFrom(core.EventTarget, core.Node, core.Node.prototype); + +// Node +inheritFrom(core.Node, core.Attr, core.Attr.prototype); +inheritFrom(core.Node, core.Document, core.Document.prototype); +inheritFrom(core.Node, core.DocumentFragment, core.DocumentFragment.prototype); +inheritFrom(core.Node, core.Element, core.Element.prototype); + + +function getDocument(el) { + return el.nodeType == core.Node.DOCUMENT_NODE ? el : el._ownerDocument; +} + +function mutationEventsEnabled(el) { + return el.nodeType != core.Node.ATTRIBUTE_NODE && + getDocument(el).implementation._hasFeature('MutationEvents'); +} + +var insertBefore_super = core.Node.prototype.insertBefore; +core.Node.prototype.insertBefore = function(newChild, refChild) { + var ret = insertBefore_super.apply(this, arguments); + if (mutationEventsEnabled(this)) { + var doc = getDocument(this), + ev = doc.createEvent("MutationEvents"); + + ev.initMutationEvent("DOMNodeInserted", true, false, this, null, null, null, null); + newChild.dispatchEvent(ev); + if (this.nodeType == core.Node.DOCUMENT_NODE || this._attachedToDocument) { + ev = doc.createEvent("MutationEvents"); + ev.initMutationEvent("DOMNodeInsertedIntoDocument", false, false, null, null, null, null, null); + core.visitTree(newChild, function(el) { + if (el.nodeType == core.Node.ELEMENT_NODE) { + el.dispatchEvent(ev); + el._attachedToDocument = true; + } + }); + } + } + return ret; +}; + +var removeChild_super = core.Node.prototype.removeChild; +core.Node.prototype.removeChild = function (oldChild) { + if (mutationEventsEnabled(this)) { + var doc = getDocument(this), + ev = doc.createEvent("MutationEvents"); + + ev.initMutationEvent("DOMNodeRemoved", true, false, this, null, null, null, null); + oldChild.dispatchEvent(ev); + + ev = doc.createEvent("MutationEvents"); + ev.initMutationEvent("DOMNodeRemovedFromDocument", false, false, null, null, null, null, null); + core.visitTree(oldChild, function(el) { + if (el.nodeType == core.Node.ELEMENT_NODE) { + el.dispatchEvent(ev); + el._attachedToDocument = false; + } + }); + } + return removeChild_super.apply(this, arguments); +}; + +function dispatchAttrEvent(doc, target, prevVal, newVal, attrName, attrChange) { + if (!newVal || newVal != prevVal) { + var ev = doc.createEvent("MutationEvents"); + ev.initMutationEvent("DOMAttrModified", true, false, target, prevVal, + newVal, attrName, attrChange); + target.dispatchEvent(ev); + } +} + +function attrNodeInterceptor(_super, change) { + return function(node) { + var target = this._parentNode, + prev = _super.apply(this, arguments); + + if (mutationEventsEnabled(target)) { + dispatchAttrEvent(target._ownerDocument, + target, + prev && prev.value || null, + change == 'ADDITION' ? node.value : null, + prev && prev.name || node.name, + core.MutationEvent.prototype[change]); + } + + return prev; + }; +} + +function attrInterceptor(_super, ns) { + return function(localName, value, _name, _prefix, _namespace) { + var target = this._parentNode, + namespace = _namespace; // do not reassign parameters when using "arguments" (performance) + + if (!mutationEventsEnabled(target)) { + _super.apply(this, arguments); + return; + } + + if (namespace === undefined) { + namespace = null; + } + + var prev = + ns ? this.$getNode(namespace, localName) : this.$getNoNS(localName); + var prevVal = prev && prev.value || null; + + _super.apply(this, arguments); + + var node = ns ? this.$getNode(namespace, localName): + this.$getNoNS(localName); + + dispatchAttrEvent(target._ownerDocument, + target, + prevVal, + node.value, + node.name, + core.MutationEvent.prototype.ADDITION); + }; +} + + +core.AttributeList.prototype.$removeNode = attrNodeInterceptor(core.AttributeList.prototype.$removeNode, 'REMOVAL'); +core.AttributeList.prototype.$setNode = attrNodeInterceptor(core.AttributeList.prototype.$setNode, 'ADDITION'); +core.AttributeList.prototype.$set = attrInterceptor(core.AttributeList.prototype.$set, true); +core.AttributeList.prototype.$setNoNS = attrInterceptor(core.AttributeList.prototype.$setNoNS, false); + +core.Document.prototype.createEvent = function(eventType) { + switch (eventType) { + case "MutationEvents": return new core.MutationEvent(eventType); + case "UIEvents": return new core.UIEvent(eventType); + case "MouseEvents": return new core.MouseEvent(eventType); + case "HTMLEvents": return new core.Event(eventType); + } + return new core.Event(eventType); +}; diff --git a/node_modules/jsdom/lib/jsdom/level2/html.js b/node_modules/jsdom/lib/jsdom/level2/html.js new file mode 100644 index 0000000..752f575 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level2/html.js @@ -0,0 +1,2140 @@ +var core = require("../level1/core"), + applyDocumentFeatures = require('../browser/documentfeatures').applyDocumentFeatures, + defineGetter = require('../utils').defineGetter, + defineSetter = require('../utils').defineSetter, + inheritFrom = require("../utils").inheritFrom, + resolveHref = require("../utils").resolveHref, + URL = require("url"), + Path = require('path'), + fs = require("fs"), + http = require('http'), + https = require('https'); + +var isBrowser = Object.prototype.toString.call(process) !== "[object process]"; + +// Setup the javascript language processor +core.languageProcessors = { + javascript : require("./languages/javascript").javascript +}; + +core.resourceLoader = { + load: function(element, href, callback) { + var ownerDoc = element._ownerDocument; + var ownerImplementation = ownerDoc.implementation; + + if (ownerImplementation._hasFeature('FetchExternalResources', element.tagName.toLowerCase())) { + var full = this.resolve(ownerDoc, href); + var url = URL.parse(full); + if (ownerImplementation._hasFeature('SkipExternalResources', full)) { + return false; + } + + var cookie = ownerDoc.cookie; + var cookieDomain = ownerDoc._cookieDomain; + var baseUrl = this.baseUrl(ownerDoc); + var enqueued = this.enqueue(element, callback, full); + + if (typeof ownerDoc._resourceLoader == 'function') { + var fetch = this.fetch.bind(this); + ownerDoc._resourceLoader.call(null, { + url: url, + cookie: cookie, + cookieDomain: cookieDomain, + baseUrl: baseUrl, + defaultFetch: function(callback) { + fetch(this.url, this.cookie, this.cookieDomain, this.baseUrl, callback); + } + }, enqueued); + } else { + this.fetch(url, cookie, cookieDomain, baseUrl, enqueued); + } + } + }, + enqueue: function(element, callback, filename) { + var loader = this, + doc = element.nodeType === core.Node.DOCUMENT_NODE ? + element : + element._ownerDocument; + + if (!doc._queue) { + return function() {}; + } + + return doc._queue.push(function(err, data) { + var ev = doc.createEvent('HTMLEvents'); + + if (!err) { + try { + callback.call(element, data, filename || doc.URL); + ev.initEvent('load', false, false); + } + catch(e) { + err = e; + } + } + + if (err) { + ev.initEvent('error', false, false); + ev.error = err; + } + + element.dispatchEvent(ev); + }); + }, + + baseUrl: function(document) { + var baseElements = document.getElementsByTagName('base'); + var baseUrl = document.URL; + + if (baseElements.length > 0) { + var baseHref = baseElements.item(0).href; + if (baseHref) { + baseUrl = resolveHref(baseUrl, baseHref); + } + } + + return baseUrl; + }, + resolve: function(document, href) { + // if getAttribute returns null, there is no href + // lets resolve to an empty string (nulls are not expected farther up) + if (href === null) { + return ''; + } + + var baseUrl = this.baseUrl(document); + + return resolveHref(baseUrl, href); + }, + fetch: function(url, cookie, cookieDomain, referrer, callback) { + if (url.hostname) { + this.download(url, cookie, cookieDomain, referrer, callback); + } else { + this.readFile(url.pathname, callback); + } + }, + download: function(url, cookie, cookieDomain, referrer, callback) { + var path = (url.pathname || '') + (url.search || ''), + options = {'method': 'GET', 'host': url.hostname, 'path': path}, + request; + if (url.protocol === 'https:') { + options.port = url.port || 443; + request = https.request(options); + } else { + options.port = url.port || 80; + request = http.request(options); + } + + // set header; accomodate browserify + if (referrer && !isBrowser) { + request.setHeader('Referer', referrer); + } + if (cookie) { + var host = url.host.split(':')[0]; + if (host.indexOf(cookieDomain, host.length - cookieDomain.length) !== -1) { + request.setHeader('cookie', cookie); + } + } + + request.on('response', function (response) { + var data = ''; + function success () { + if ([301, 302, 303, 307].indexOf(response.statusCode) > -1) { + var redirect = URL.resolve(url, response.headers["location"]); + core.resourceLoader.download(URL.parse(redirect), cookie, cookieDomain, referrer, callback); + } else { + callback(null, data); + } + } + + // Accomodate browserify + if (response.setEncoding) { + response.setEncoding('utf8'); + } + + response.on('data', function (chunk) { + data += chunk.toString(); + }); + response.on('end', function() { + // According to node docs, 'close' can fire after 'end', but not + // vice versa. Remove 'close' listener so we don't call success twice. + response.removeAllListeners('close'); + success(); + }); + response.on('close', function (err) { + if (err) { + callback(err); + } else { + success(); + } + }); + }); + + request.on('error', callback); + request.end(); + }, + readFile: function(url, callback) { + fs.readFile(url.replace(/^file:\/\//, "").replace(/^\/([a-z]):\//i, '$1:/').replace(/%20/g, ' '), 'utf8', callback); + } +}; + +function define(elementClass, def) { + var tagName = def.tagName, + tagNames = def.tagNames || (tagName? [tagName] : []), + parentClass = def.parentClass || core.HTMLElement, + attrs = def.attributes || [], + proto = def.proto || {}; + + var elem = core[elementClass] = function(document, name) { + parentClass.call(this, document, name || tagName.toUpperCase()); + if (elem._init) { + elem._init.call(this); + } + }; + elem._init = def.init; + + inheritFrom(parentClass, elem, proto); + + attrs.forEach(function(n) { + var prop = n.prop || n, + attr = n.attr || prop.toLowerCase(); + + if (!n.prop || n.read !== false) { + defineGetter(elem.prototype, prop, function() { + var s = this.getAttribute(attr); + if (n.type && n.type === 'boolean') { + return s !== null; + } + if (n.type && n.type === 'long') { + return +s; + } + if (typeof n === 'object' && n.normalize) { // see GH-491 + return n.normalize(s); + } + if (s === null) { + s = ''; + } + return s; + }); + } + + if (!n.prop || n.write !== false) { + defineSetter(elem.prototype, prop, function(val) { + if (!val) { + this.removeAttribute(attr); + } + else { + var s = val.toString(); + if (typeof n === 'object' && n.normalize) { + s = n.normalize(s); + } + this.setAttribute(attr, s); + } + }); + } + }); + + tagNames.forEach(function(tag) { + core.Document.prototype._elementBuilders[tag.toLowerCase()] = function(doc, s) { + var el = new elem(doc, s); + + if (def.elementBuilder) { + return def.elementBuilder(el, doc, s); + } + + return el; + }; + }); +} + + + +core.HTMLCollection = function HTMLCollection(element, query) { + this._keys = []; + core.NodeList.call(this, element, query); +}; +inheritFrom(core.NodeList, core.HTMLCollection, { + namedItem: function(name) { + // Try property shortcut; should work in most cases + if (Object.prototype.hasOwnProperty.call(this, name)) { + return this[name]; + } + + var results = this._toArray(), + l = results.length, + node, + matchingName = null; + + for (var i=0; i<l; i++) { + node = results[i]; + if (node.getAttribute('id') === name) { + return node; + } else if (node.getAttribute('name') === name) { + matchingName = node; + } + } + return matchingName; + }, + toString: function() { + return '[ jsdom HTMLCollection ]: contains ' + this.length + ' items'; + }, + _resetTo: function(array) { + var i, _this = this; + + for (i = 0; i < this._keys.length; ++i) { + delete this[this._keys[i]]; + } + this._keys = []; + + core.NodeList.prototype._resetTo.apply(this, arguments); + + function testAttr(node, attr) { + var val = node.getAttribute(attr); + if (val && !Object.prototype.hasOwnProperty.call(_this, val)) { + _this[val] = node; + _this._keys.push(val); + } + } + for (i = 0; i < array.length; ++i) { + testAttr(array[i], 'id'); + } + for (i = 0; i < array.length; ++i) { + testAttr(array[i], 'name'); + } + } +}); +Object.defineProperty(core.HTMLCollection.prototype, 'constructor', { + value: core.NodeList, + writable: true, + configurable: true +}); + +core.HTMLOptionsCollection = core.HTMLCollection; + +function closest(e, tagName) { + tagName = tagName.toUpperCase(); + while (e) { + if (e.nodeName.toUpperCase() === tagName || + (e.tagName && e.tagName.toUpperCase() === tagName)) + { + return e; + } + e = e._parentNode; + } + return null; +} + +function descendants(e, tagName, recursive) { + var owner = recursive ? e._ownerDocument || e : e; + return new core.HTMLCollection(owner, core.mapper(e, function(n) { + return n.tagName === tagName; + }, recursive)); +} + +function firstChild(e, tagName) { + if (!e) { + return null; + } + var c = descendants(e, tagName, false); + return c.length > 0 ? c[0] : null; +} + +function ResourceQueue(paused) { + this.paused = !!paused; +} +ResourceQueue.prototype = { + push: function(callback) { + var q = this; + var item = { + prev: q.tail, + check: function() { + if (!q.paused && !this.prev && this.fired){ + callback(this.err, this.data); + if (this.next) { + this.next.prev = null; + this.next.check(); + }else{//q.tail===this + q.tail = null; + } + } + } + }; + if (q.tail) { + q.tail.next = item; + } + q.tail = item; + return function(err, data) { + item.fired = 1; + item.err = err; + item.data = data; + item.check(); + }; + }, + resume: function() { + if(!this.paused){ + return; + } + this.paused = false; + var head = this.tail; + while(head && head.prev){ + head = head.prev; + } + if(head){ + head.check(); + } + } +}; + +core.HTMLDocument = function HTMLDocument(options) { + core.Document.call(this, options); + this._referrer = options.referrer; + this._cookie = options.cookie; + this._cookieDomain = options.cookieDomain || '127.0.0.1'; + this._documentRoot = options.documentRoot || Path.dirname(this._URL); + this._queue = new ResourceQueue(options.deferClose); + this._resourceLoader = options.resourceLoader; + this.readyState = 'loading'; + + // Add level2 features + this.implementation._addFeature('core' , '2.0'); + this.implementation._addFeature('html' , '2.0'); + this.implementation._addFeature('xhtml' , '2.0'); + this.implementation._addFeature('xml' , '2.0'); +}; + +inheritFrom(core.Document, core.HTMLDocument, { + _referrer : "", + get referrer() { + return this._referrer || ''; + }, + get domain() { + return ""; + }, + get images() { + return this.getElementsByTagName('IMG'); + }, + get applets() { + return new core.HTMLCollection(this, core.mapper(this, function(el) { + if (el && el.tagName) { + var upper = el.tagName.toUpperCase(); + if (upper === "APPLET") { + return true; + } else if (upper === "OBJECT" && + el.getElementsByTagName('APPLET').length > 0) + { + return true; + } + } + })); + }, + get links() { + return new core.HTMLCollection(this, core.mapper(this, function(el) { + if (el && el.tagName) { + var upper = el.tagName.toUpperCase(); + if (upper === "AREA" || (upper === "A" && el.href)) { + return true; + } + } + })); + }, + get forms() { + return this.getElementsByTagName('FORM'); + }, + get anchors() { + return this.getElementsByTagName('A'); + }, + open : function() { + this._childNodes = []; + this._documentElement = null; + this._modified(); + }, + close : function() { + this._queue.resume(); + // Set the readyState to 'complete' once all resources are loaded. + // As a side-effect the document's load-event will be dispatched. + core.resourceLoader.enqueue(this, function() { + this.readyState = 'complete'; + var ev = this.createEvent('HTMLEvents'); + ev.initEvent('DOMContentLoaded', false, false); + this.dispatchEvent(ev); + })(null, true); + }, + + // document.write is defined in browser/index.js. + + writeln : function(text) { + this.write(text + '\n'); + }, + + getElementsByName : function(elementName) { + return new core.HTMLCollection(this, core.mapper(this, function(el) { + return (el.getAttribute && el.getAttribute("name") === elementName); + })); + }, + + get title() { + var head = this.head, + title = head ? firstChild(head, 'TITLE') : null; + return title ? title.textContent : ''; + }, + + set title(val) { + var title = firstChild(this.head, 'TITLE'); + if (!title) { + title = this.createElement('TITLE'); + var head = this.head; + if (!head) { + head = this.createElement('HEAD'); + this.documentElement.insertBefore(head, this.documentElement.firstChild); + } + head.appendChild(title); + } + title.textContent = val; + }, + + get head() { + return firstChild(this.documentElement, 'HEAD'); + }, + + set head(unused) { /* noop */ }, + + get body() { + var body = firstChild(this.documentElement, 'BODY'); + if (!body) { + body = firstChild(this.documentElement, 'FRAMESET'); + } + return body; + }, + + _cookie : "", + get cookie() { + var cookies = Array.isArray(this._cookie) ? + this._cookie : + (this._cookie && this._cookie.length > 0 ? [this._cookie] : []); + + return cookies.map(function (x) { + return x.split(';')[0]; + }).join('; '); + }, + set cookie(val) { + if (val == null) return val; + var key = val.split('=')[0]; + var cookies = Array.isArray(this._cookie) ? + this._cookie : + (this._cookie && this._cookie.length > 0 ? [this._cookie] : []); + for (var i = 0; i < cookies.length; i++) { + if (cookies[i].lastIndexOf(key + '=', 0) === 0) { + cookies[i] = val; + key = null; + break; + } + } + if (key) { + cookies.push(val); + } + if (cookies.length === 1) { + this._cookie = cookies[0]; + } else { + this._cookie = cookies; + } + return val; + } +}); + +define('HTMLElement', { + parentClass: core.Element, + proto : { + // Add default event behavior (click link to navigate, click button to submit + // form, etc). We start by wrapping dispatchEvent so we can forward events to + // the element's _eventDefault function (only events that did not incur + // preventDefault). + dispatchEvent : function (event) { + var outcome = core.Node.prototype.dispatchEvent.call(this, event) + + if (!event._preventDefault && + event.target._eventDefaults[event.type] && + typeof event.target._eventDefaults[event.type] === 'function') + { + event.target._eventDefaults[event.type](event) + } + return outcome; + }, + getBoundingClientRect: function () { + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0 + }; + }, + focus : function() { + this._ownerDocument.activeElement = this; + }, + blur : function() { + this._ownerDocument.activeElement = this._ownerDocument.body; + }, + _eventDefaults : {} + }, + attributes: [ + 'id', + 'title', + 'lang', + 'dir', + {prop: 'className', attr: 'class', normalize: function(s) { return s || ''; }} + ] +}); + +core.Document.prototype._defaultElementBuilder = function(document, tagName) { + return new core.HTMLElement(document, tagName); +}; + +// http://www.whatwg.org/specs/web-apps/current-work/#category-listed +var listedElements = /button|fieldset|input|keygen|object|select|textarea/i; + +define('HTMLFormElement', { + tagName: 'FORM', + proto: { + _descendantAdded: function(parent, child) { + var form = this; + core.visitTree(child, function(el) { + if (typeof el._changedFormOwner === 'function') { + el._changedFormOwner(form); + } + }); + + core.HTMLElement.prototype._descendantAdded.apply(this, arguments); + }, + _descendantRemoved: function(parent, child) { + core.visitTree(child, function(el) { + if (typeof el._changedFormOwner === 'function') { + el._changedFormOwner(null); + } + }); + + core.HTMLElement.prototype._descendantRemoved.apply(this, arguments); + }, + get elements() { + return new core.HTMLCollection(this._ownerDocument, core.mapper(this, function(e) { + return listedElements.test(e.nodeName) ; // TODO exclude <input type="image"> + })); + }, + get length() { + return this.elements.length; + }, + _dispatchSubmitEvent: function() { + var ev = this._ownerDocument.createEvent('HTMLEvents'); + ev.initEvent('submit', true, true); + if (!this.dispatchEvent(ev)) { + this.submit(); + }; + }, + submit: function() { + }, + reset: function() { + this.elements._toArray().forEach(function(el) { + if (typeof el._formReset === 'function') { + el._formReset(); + } + }); + } + }, + attributes: [ + 'name', + {prop: 'acceptCharset', attr: 'accept-charset'}, + 'action', + 'enctype', + 'method', + 'target' + ] +}); + +define('HTMLLinkElement', { + tagName: 'LINK', + proto: { + get href() { + return core.resourceLoader.resolve(this._ownerDocument, this.getAttribute('href')); + } + }, + attributes: [ + {prop: 'disabled', type: 'boolean'}, + 'charset', + 'href', + 'hreflang', + 'media', + 'rel', + 'rev', + 'target', + 'type' + ] +}); + +define('HTMLMetaElement', { + tagName: 'META', + attributes: [ + 'content', + {prop: 'httpEquiv', attr: 'http-equiv'}, + 'name', + 'scheme' + ] +}); + +define('HTMLHtmlElement', { + tagName: 'HTML', + attributes: [ + 'version' + ] +}); + +define('HTMLHeadElement', { + tagName: 'HEAD', + attributes: [ + 'profile' + ] +}); + +define('HTMLTitleElement', { + tagName: 'TITLE', + proto: { + get text() { + return this.innerHTML; + }, + set text(s) { + this.innerHTML = s; + } + } +}); + +define('HTMLBaseElement', { + tagName: 'BASE', + attributes: [ + 'href', + 'target' + ] +}); + + +//**Deprecated** +define('HTMLIsIndexElement', { + tagName : 'ISINDEX', + parentClass : core.Element, + proto : { + get form() { + return closest(this, 'FORM'); + } + }, + attributes : [ + 'prompt' + ] +}); + + +define('HTMLStyleElement', { + tagName: 'STYLE', + attributes: [ + {prop: 'disabled', type: 'boolean'}, + 'media', + 'type', + ] +}); + +define('HTMLBodyElement', { + proto: (function() { + var proto = {}; + // The body element's "traditional" event handlers are proxied to the + // window object. + // See: http://www.whatwg.org/specs/web-apps/current-work/#the-body-element + ['onafterprint', 'onbeforeprint', 'onbeforeunload', 'onblur', 'onerror', + 'onfocus', 'onhashchange', 'onload', 'onmessage', 'onoffline', 'ononline', + 'onpagehide', 'onpageshow', 'onpopstate', 'onresize', 'onscroll', + 'onstorage', 'onunload'].forEach(function (name) { + defineSetter(proto, name, function (handler) { + this._ownerDocument.parentWindow[name] = handler; + }); + defineGetter(proto, name, function () { + return this._ownerDocument.parentWindow[name]; + }); + }); + return proto; + })(), + tagName: 'BODY', + attributes: [ + 'aLink', + 'background', + 'bgColor', + 'link', + 'text', + 'vLink' + ] +}); + +define('HTMLSelectElement', { + tagName: 'SELECT', + proto: { + _formReset: function() { + this.options._toArray().forEach(function(option, i) { + option._selectedness = option.defaultSelected; + option._dirtyness = false; + }); + this._askedForAReset(); + }, + _askedForAReset: function() { + if (this.hasAttribute('multiple')) { + return; + } + + var options = this.options._toArray(); + var selected = options.filter(function(option){ + return option._selectedness; + }); + + // size = 1 is default if not multiple + if ((!this.size || this.size === 1) && !selected.length) { + // select the first option that is not disabled + for (var i = 0; i < options.length; ++i) { + var option = options[i]; + var disabled = option.disabled; + if (option._parentNode && + option._parentNode.nodeName.toUpperCase() === 'OPTGROUP' && + option._parentNode.disabled) { + disabled = true; + } + + if (!disabled) { + // (do not set dirty) + option._selectedness = true; + break; + } + } + } else if (selected.length >= 2) { + // select the last selected option + selected.forEach(function(option, index) { + option._selectedness = index === selected.length - 1; + }); + } + }, + _descendantAdded: function(parent, child) { + if (child.nodeType === core.Node.ELEMENT_NODE) { + this._askedForAReset(); + } + + core.HTMLElement.prototype._descendantAdded.apply(this, arguments); + }, + _descendantRemoved: function(parent, child) { + if (child.nodeType === core.Node.ELEMENT_NODE) { + this._askedForAReset(); + } + + core.HTMLElement.prototype._descendantRemoved.apply(this, arguments); + }, + _attrModified: function(name, value) { + if (name === 'multiple' || name === 'size') { + this._askedForAReset(); + } + core.HTMLElement.prototype._attrModified.apply(this, arguments); + }, + get options() { + return new core.HTMLOptionsCollection(this, core.mapper(this, function(n) { + return n.nodeName === 'OPTION'; + })); + }, + + get length() { + return this.options.length; + }, + + get selectedIndex() { + return this.options._toArray().reduceRight(function(prev, option, i) { + return option.selected ? i : prev; + }, -1); + }, + + set selectedIndex(index) { + this.options._toArray().forEach(function(option, i) { + option.selected = i === index; + }); + }, + + get value() { + var i = this.selectedIndex; + if (this.options.length && (i === -1)) { + i = 0; + } + if (i === -1) { + return ''; + } + return this.options[i].value; + }, + + set value(val) { + var self = this; + this.options._toArray().forEach(function(option) { + if (option.value === val) { + option.selected = true; + } else { + if (!self.hasAttribute('multiple')) { + // Remove the selected bit from all other options in this group + // if the multiple attr is not present on the select + option.selected = false; + } + } + }); + }, + + get form() { + return closest(this, 'FORM'); + }, + + get type() { + return this.multiple ? 'select-multiple' : 'select-one'; + }, + + add: function(opt, before) { + if (before) { + this.insertBefore(opt, before); + } + else { + this.appendChild(opt); + } + }, + + remove: function(index) { + var opts = this.options._toArray(); + if (index >= 0 && index < opts.length) { + var el = opts[index]; + el._parentNode.removeChild(el); + } + } + + }, + attributes: [ + {prop: 'disabled', type: 'boolean'}, + {prop: 'multiple', type: 'boolean'}, + 'name', + {prop: 'size', type: 'long'}, + {prop: 'tabIndex', type: 'long'}, + ] +}); + +define('HTMLOptGroupElement', { + tagName: 'OPTGROUP', + attributes: [ + {prop: 'disabled', type: 'boolean'}, + 'label' + ] +}); + +define('HTMLOptionElement', { + tagName: 'OPTION', + proto: { + // whenever selectedness is set to true, make sure all + // other options set selectedness to false + _selectedness: false, + _dirtyness: false, + _removeOtherSelectedness: function() { + //Remove the selectedness flag from all other options in this select + var select = this._selectNode; + + if (select && !select.multiple) { + var o = select.options; + for (var i = 0; i < o.length; i++) { + if (o[i] !== this) { + o[i]._selectedness = false; + } + } + } + }, + _askForAReset: function() { + var select = this._selectNode; + if (select) { + select._askedForAReset(); + } + }, + _attrModified: function(name, value) { + if (!this._dirtyness && name === 'selected') { + this._selectedness = this.defaultSelected; + if (this._selectedness) { + this._removeOtherSelectedness(); + } + this._askForAReset(); + } + core.HTMLElement.prototype._attrModified.apply(this, arguments); + }, + get _selectNode() { + var select = this._parentNode; + if (!select) return null; + if (select.nodeName.toUpperCase() !== 'SELECT') { + select = select._parentNode; + if (!select) return null; + if (select.nodeName.toUpperCase() !== 'SELECT') return null; + } + return select; + }, + get form() { + return closest(this, 'FORM'); + }, + get defaultSelected() { + return this.getAttribute('selected') !== null; + }, + set defaultSelected(s) { + if (s) this.setAttribute('selected', 'selected'); + else this.removeAttribute('selected'); + }, + get text() { + return this.innerHTML; + }, + get value() { + return (this.hasAttribute('value')) ? this.getAttribute('value') : this.innerHTML; + }, + set value(val) { + this.setAttribute('value', val); + }, + get index() { + return closest(this, 'SELECT').options._toArray().indexOf(this); + }, + get selected() { + return this._selectedness; + }, + set selected(s) { + this._dirtyness = true; + this._selectedness = !!s; + if (this._selectedness) { + this._removeOtherSelectedness(); + } + this._askForAReset(); + } + }, + attributes: [ + {prop: 'disabled', type: 'boolean'}, + 'label' + ] +}); + +define('HTMLInputElement', { + tagName: 'INPUT', + init: function() { + if (!this.type) { + this.type = 'text'; + } + }, + proto: { + _value: null, + _dirtyValue: false, + _checkedness: false, + _dirtyCheckedness: false, + _attrModified: function(name, value) { + if (!this._dirtyValue && name === 'value') { + this._value = this.defaultValue; + } + if (!this._dirtyCheckedness && name === 'checked') { + this._checkedness = this.defaultChecked; + if (this._checkedness) { + this._removeOtherRadioCheckedness(); + } + } + + if (name === 'name' || name === 'type') { + if (this._checkedness) { + this._removeOtherRadioCheckedness(); + } + } + + core.HTMLElement.prototype._attrModified.apply(this, arguments); + }, + _formReset: function() { + this._value = this.defaultValue; + this._dirtyValue = false; + this._checkedness = this.defaultChecked; + this._dirtyCheckedness = false; + if (this._checkedness) { + this._removeOtherRadioCheckedness(); + } + }, + _changedFormOwner: function(newForm) { + if (this._checkedness) { + this._removeOtherRadioCheckedness(); + } + }, + _removeOtherRadioCheckedness: function() { + var root = this._radioButtonGroupRoot; + if (!root) { + return; + } + + var name = this.name.toLowerCase(); + var radios = new core.HTMLCollection(this, core.mapper(root, function(el) { + return el.type === 'radio' && + el.name && + el.name.toLowerCase() === name && + el._radioButtonGroupRoot === root; + })); + + radios._toArray().forEach(function(radio) { + if (radio !== this) { + radio._checkedness = false; + } + }, this); + }, + get _radioButtonGroupRoot() { + if (this.type !== 'radio' || !this.name) { + return null; + } + + var e = this._parentNode; + while (e) { + // root node of this home sub tree + // or the form element we belong to + if (!e._parentNode || e.nodeName.toUpperCase() === 'FORM') { + return e; + } + e = e._parentNode; + } + return null; + }, + get form() { + return closest(this, 'FORM'); + }, + get defaultValue() { + var val = this.getAttribute('value'); + return val !== null ? val : ""; + }, + set defaultValue(val) { + this.setAttribute('value', String(val)); + }, + get defaultChecked() { + return this.getAttribute('checked') !== null; + }, + set defaultChecked(s) { + if (s) this.setAttribute('checked', 'checked'); + else this.removeAttribute('checked'); + }, + get checked() { + return this._checkedness; + }, + set checked(checked) { + this._checkedness = !!checked; + this._dirtyCheckedness = true; + if (this._checkedness) { + this._removeOtherRadioCheckedness(); + } + }, + get value() { + if (this._value === null) { + return ''; + } + return this._value; + }, + set value(val) { + this._dirtyValue = true; + if (val === null) { + this._value = null; + } else { + this._value = String(val); + } + }, + get type() { + var type = this.getAttribute('type'); + return type ? type : 'text'; + }, + set type(type) { + this.setAttribute('type', type); + }, + select: function() { + }, + + _dispatchClickEvent: function() { + var event = this._ownerDocument.createEvent("HTMLEvents"); + event.initEvent("click", true, true); + this.dispatchEvent(event); + }, + + click: function() { + if (this.type === 'checkbox') { + this.checked = !this.checked; + } + else if (this.type === 'radio') { + this.checked = true; + } + else if (this.type === 'submit') { + var form = this.form; + if (form) { + form._dispatchSubmitEvent(); + } + } + this._dispatchClickEvent(); + } + }, + attributes: [ + 'accept', + 'accessKey', + 'align', + 'alt', + {prop: 'disabled', type: 'boolean'}, + {prop: 'maxLength', type: 'long'}, + 'name', + {prop: 'readOnly', type: 'boolean'}, + {prop: 'size', type: 'long'}, + 'src', + {prop: 'tabIndex', type: 'long'}, + {prop: 'type', normalize: function(val) { + return val ? val.toLowerCase() : 'text'; + }}, + 'useMap' + ] +}); + +define('HTMLTextAreaElement', { + tagName: 'TEXTAREA', + proto: { + _apiValue: null, + _dirtyValue: false, + // "raw value" and "value" are not used here because jsdom has no GUI + _formReset: function() { + this._apiValue = null; + this._dirtyValue = false; + }, + get form() { + return closest(this, 'FORM'); + }, + get defaultValue() { + return this.textContent; + }, + set defaultValue(val) { + this.textContent = val; + }, + get value() { + // The WHATWG specifies that when "textContent" changes, the "raw value" + // (just the API value in jsdom) must also be updated. + // This slightly different solution has identical results, but is a lot less complex. + if (this._dirtyValue) { + if (this._apiValue === null) { + return ''; + } + return this._apiValue; + } + + var val = this.defaultValue; + val = val.replace(/\r\n|\r/g, '\n'); // API value normalizes line breaks per WHATWG + return val; + }, + set value(val) { + if (val) { + val = val.replace(/\r\n|\r/g, '\n'); // API value normalizes line breaks per WHATWG + } + + this._dirtyValue = true; + this._apiValue = val; + }, + get textLength() { + return this.value.length; // code unit length (16 bit) + }, + get type() { + return 'textarea'; + }, + select: function() { + } + }, + attributes: [ + 'accessKey', + {prop: 'cols', type: 'long'}, + {prop: 'disabled', type: 'boolean'}, + {prop: 'maxLength', type: 'long'}, + 'name', + {prop: 'readOnly', type: 'boolean'}, + {prop: 'rows', type: 'long'}, + {prop: 'tabIndex', type: 'long'} + ] +}); + +define('HTMLButtonElement', { + tagName: 'BUTTON', + proto: { + get form() { + return closest(this, 'FORM'); + } + }, + attributes: [ + 'accessKey', + {prop: 'disabled', type: 'boolean'}, + 'name', + {prop: 'tabIndex', type: 'long'}, + 'type', + 'value' + ] +}); + +define('HTMLLabelElement', { + tagName: 'LABEL', + proto: { + get form() { + return closest(this, 'FORM'); + } + }, + attributes: [ + 'accessKey', + {prop: 'htmlFor', attr: 'for'} + ] +}); + +define('HTMLFieldSetElement', { + tagName: 'FIELDSET', + proto: { + get form() { + return closest(this, 'FORM'); + } + } +}); + +define('HTMLLegendElement', { + tagName: 'LEGEND', + proto: { + get form() { + return closest(this, 'FORM'); + } + }, + attributes: [ + 'accessKey', + 'align' + ] +}); + +define('HTMLUListElement', { + tagName: 'UL', + attributes: [ + {prop: 'compact', type: 'boolean'}, + 'type' + ] +}); + +define('HTMLOListElement', { + tagName: 'OL', + attributes: [ + {prop: 'compact', type: 'boolean'}, + {prop: 'start', type: 'long'}, + 'type' + ] +}); + +define('HTMLDListElement', { + tagName: 'DL', + attributes: [ + {prop: 'compact', type: 'boolean'} + ] +}); + +define('HTMLDirectoryElement', { + tagName: 'DIR', + attributes: [ + {prop: 'compact', type: 'boolean'} + ] +}); + +define('HTMLMenuElement', { + tagName: 'MENU', + attributes: [ + {prop: 'compact', type: 'boolean'} + ] +}); + +define('HTMLLIElement', { + tagName: 'LI', + attributes: [ + 'type', + {prop: 'value', type: 'long'} + ] +}); + +define('HTMLCanvasElement', { + tagName: 'CANVAS', + attributes: [ + 'align' + ], + elementBuilder: function (element) { + // require node-canvas and catch the error if it blows up + try { + var canvas = new (require('canvas'))(0,0); + for (var attr in element) { + if (!canvas[attr]) { + canvas[attr] = element[attr]; + } + } + return canvas; + } catch (e) { + return element; + } + } +}); + +define('HTMLDivElement', { + tagName: 'DIV', + attributes: [ + 'align' + ], + proto: { + toString: function() { return '[object HTMLDivElement]'; } + } +}); + +define('HTMLParagraphElement', { + tagName: 'P', + attributes: [ + 'align' + ] +}); + +define('HTMLHeadingElement', { + tagNames: ['H1','H2','H3','H4','H5','H6'], + attributes: [ + 'align' + ] +}); + +define('HTMLQuoteElement', { + tagNames: ['Q','BLOCKQUOTE'], + attributes: [ + 'cite' + ] +}); + +define('HTMLPreElement', { + tagName: 'PRE', + attributes: [ + {prop: 'width', type: 'long'} + ] +}); + +define('HTMLBRElement', { + tagName: 'BR', + attributes: [ + 'clear' + ] +}); + +define('HTMLBaseFontElement', { + tagName: 'BASEFONT', + attributes: [ + 'color', + 'face', + {prop: 'size', type: 'long'} + ] +}); + +define('HTMLFontElement', { + tagName: 'FONT', + attributes: [ + 'color', + 'face', + 'size' + ] +}); + +define('HTMLHRElement', { + tagName: 'HR', + attributes: [ + 'align', + {prop: 'noShade', type: 'boolean'}, + 'size', + 'width' + ] +}); + +define('HTMLModElement', { + tagNames: ['INS', 'DEL'], + attributes: [ + 'cite', + 'dateTime' + ] +}); + +define('HTMLAnchorElement', { + tagName: 'A', + + proto: { + get href() { + return core.resourceLoader.resolve(this._ownerDocument, this.getAttribute('href')); + }, + get hostname() { + return URL.parse(this.href).hostname || ''; + }, + get host() { + return URL.parse(this.href).host || ''; + }, + get origin() { + var proto = URL.parse(this.href).protocol; + + if (proto !== undefined && proto !== null) { + proto += '//'; + } + + return proto + URL.parse(this.href).host || ''; + }, + get port() { + return URL.parse(this.href).port || ''; + }, + get protocol() { + var protocol = URL.parse(this.href).protocol; + return (protocol == null) ? ':' : protocol; + }, + get password() { + var auth = URL.parse(this.href).auth; + return auth.substr(auth.indexOf(':') + 1); + }, + get pathname() { + return URL.parse(this.href).pathname || ''; + }, + get username() { + var auth = URL.parse(this.href).auth; + return auth.substr(0, auth.indexOf(':')); + }, + get search() { + return URL.parse(this.href).search || ''; + }, + get hash() { + return URL.parse(this.href).hash || ''; + } + }, + attributes: [ + 'accessKey', + 'charset', + 'coords', + {prop: 'href', type: 'string', read: false}, + 'hreflang', + 'name', + 'rel', + 'rev', + 'shape', + {prop: 'tabIndex', type: 'long'}, + 'target', + 'type' + ] +}); + +define('HTMLImageElement', { + tagName: 'IMG', + proto: { + _attrModified: function(name, value, oldVal) { + if (name == 'src' && value !== oldVal) { + core.resourceLoader.enqueue(this, function() {})(); + } + }, + get src() { + return core.resourceLoader.resolve(this._ownerDocument, this.getAttribute('src')); + } + }, + attributes: [ + 'name', + 'align', + 'alt', + 'border', + {prop: 'height', type: 'long'}, + {prop: 'hspace', type: 'long'}, + {prop: 'isMap', type: 'boolean'}, + 'longDesc', + {prop: 'src', type: 'string', read: false}, + 'useMap', + {prop: 'vspace', type: 'long'}, + {prop: 'width', type: 'long'} + ] +}); + +define('HTMLObjectElement', { + tagName: 'OBJECT', + proto: { + get form() { + return closest(this, 'FORM'); + }, + get contentDocument() { + return null; + } + }, + attributes: [ + 'code', + 'align', + 'archive', + 'border', + 'codeBase', + 'codeType', + 'data', + {prop: 'declare', type: 'boolean'}, + {prop: 'height', type: 'long'}, + {prop: 'hspace', type: 'long'}, + 'name', + 'standby', + {prop: 'tabIndex', type: 'long'}, + 'type', + 'useMap', + {prop: 'vspace', type: 'long'}, + {prop: 'width', type: 'long'} + ] +}); + +define('HTMLParamElement', { + tagName: 'PARAM', + attributes: [ + 'name', + 'type', + 'value', + 'valueType' + ] +}); + +define('HTMLAppletElement', { + tagName: 'APPLET', + attributes: [ + 'align', + 'alt', + 'archive', + 'code', + 'codeBase', + 'height', + {prop: 'hspace', type: 'long'}, + 'name', + 'object', + {prop: 'vspace', type: 'long'}, + 'width' + ] +}); + +define('HTMLMapElement', { + tagName: 'MAP', + proto: { + get areas() { + return this.getElementsByTagName("AREA"); + } + }, + attributes: [ + 'name' + ] +}); + +define('HTMLAreaElement', { + tagName: 'AREA', + attributes: [ + 'accessKey', + 'alt', + 'coords', + 'href', + {prop: 'noHref', type: 'boolean'}, + 'shape', + {prop: 'tabIndex', type: 'long'}, + 'target' + ] +}); + +define('HTMLScriptElement', { + tagName: 'SCRIPT', + init: function() { + this.addEventListener('DOMNodeInsertedIntoDocument', function() { + if (this.src) { + core.resourceLoader.load(this, this.src, this._eval); + } + else { + var src = this.sourceLocation || {}, + filename = src.file || this._ownerDocument.URL; + + if (src) { + filename += ':' + src.line + ':' + src.col; + } + filename += '<script>'; + + core.resourceLoader.enqueue(this, this._eval, filename)(null, this.text); + } + }); + }, + proto: { + _eval: function(text, filename) { + if (this._ownerDocument.implementation._hasFeature("ProcessExternalResources", "script") && + this.language && + core.languageProcessors[this.language]) + { + this._ownerDocument._writeAfterElement = this; + core.languageProcessors[this.language](this, text, filename); + delete this._ownerDocument._writeAfterElement; + } + }, + get language() { + var type = this.type || "text/javascript"; + return type.split("/").pop().toLowerCase(); + }, + get text() { + var i=0, children = this._childNodes, l = children.length, ret = []; + + for (i; i<l; i++) { + ret.push(children[i].nodeValue); + } + + return ret.join(""); + }, + set text(text) { + while (this._childNodes.length) { + this.removeChild(this._childNodes[this._childNodes.length-1]); + } + this.appendChild(this._ownerDocument.createTextNode(text)); + } + }, + attributes : [ + {prop: 'defer', 'type': 'boolean'}, + 'htmlFor', + 'event', + 'charset', + 'type', + 'src' + ] +}) + +define('HTMLTableElement', { + tagName: 'TABLE', + proto: { + get caption() { + return firstChild(this, 'CAPTION'); + }, + get tHead() { + return firstChild(this, 'THEAD'); + }, + get tFoot() { + return firstChild(this, 'TFOOT'); + }, + get rows() { + if (!this._rows) { + var table = this; + this._rows = new core.HTMLCollection(this._ownerDocument, function() { + var sections = [table.tHead].concat(table.tBodies._toArray(), table.tFoot).filter(function(s) { return !!s }); + + if (sections.length === 0) { + return core.mapDOMNodes(table, false, function(el) { + return el.tagName === 'TR'; + }); + } + + return sections.reduce(function(prev, s) { + return prev.concat(s.rows._toArray()); + }, []); + + }); + } + return this._rows; + }, + get tBodies() { + if (!this._tBodies) { + this._tBodies = descendants(this, 'TBODY', false); + } + return this._tBodies; + }, + createTHead: function() { + var el = this.tHead; + if (!el) { + el = this._ownerDocument.createElement('THEAD'); + this.appendChild(el); + } + return el; + }, + deleteTHead: function() { + var el = this.tHead; + if (el) { + el._parentNode.removeChild(el); + } + }, + createTFoot: function() { + var el = this.tFoot; + if (!el) { + el = this._ownerDocument.createElement('TFOOT'); + this.appendChild(el); + } + return el; + }, + deleteTFoot: function() { + var el = this.tFoot; + if (el) { + el._parentNode.removeChild(el); + } + }, + createCaption: function() { + var el = this.caption; + if (!el) { + el = this._ownerDocument.createElement('CAPTION'); + this.appendChild(el); + } + return el; + }, + deleteCaption: function() { + var c = this.caption; + if (c) { + c._parentNode.removeChild(c); + } + }, + insertRow: function(index) { + var tr = this._ownerDocument.createElement('TR'); + if (this._childNodes.length === 0) { + this.appendChild(this._ownerDocument.createElement('TBODY')); + } + var rows = this.rows._toArray(); + if (index < -1 || index > rows.length) { + throw new core.DOMException(core.INDEX_SIZE_ERR); + } + if (index === -1 || (index === 0 && rows.length === 0)) { + this.tBodies.item(0).appendChild(tr); + } + else if (index === rows.length) { + var ref = rows[index-1]; + ref._parentNode.appendChild(tr); + } + else { + var ref = rows[index]; + ref._parentNode.insertBefore(tr, ref); + } + return tr; + }, + deleteRow: function(index) { + var rows = this.rows._toArray(), l = rows.length; + if (index === -1) { + index = l-1; + } + if (index < 0 || index >= l) { + throw new core.DOMException(core.INDEX_SIZE_ERR); + } + var tr = rows[index]; + tr._parentNode.removeChild(tr); + } + }, + attributes: [ + 'align', + 'bgColor', + 'border', + 'cellPadding', + 'cellSpacing', + 'frame', + 'rules', + 'summary', + 'width' + ] +}); + +define('HTMLTableCaptionElement', { + tagName: 'CAPTION', + attributes: [ + 'align' + ] +}); + +define('HTMLTableColElement', { + tagNames: ['COL','COLGROUP'], + attributes: [ + 'align', + {prop: 'ch', attr: 'char'}, + {prop: 'chOff', attr: 'charoff'}, + {prop: 'span', type: 'long'}, + 'vAlign', + 'width', + ] +}); + +define('HTMLTableSectionElement', { + tagNames: ['THEAD','TBODY','TFOOT'], + proto: { + get rows() { + if (!this._rows) { + this._rows = descendants(this, 'TR', false); + } + return this._rows; + }, + insertRow: function(index) { + var tr = this._ownerDocument.createElement('TR'); + var rows = this.rows._toArray(); + if (index < -1 || index > rows.length) { + throw new core.DOMException(core.INDEX_SIZE_ERR); + } + if (index === -1 || index === rows.length) { + this.appendChild(tr); + } + else { + var ref = rows[index]; + this.insertBefore(tr, ref); + } + return tr; + }, + deleteRow: function(index) { + var rows = this.rows._toArray(); + if (index === -1) { + index = rows.length-1; + } + if (index < 0 || index >= rows.length) { + throw new core.DOMException(core.INDEX_SIZE_ERR); + } + var tr = this.rows[index]; + this.removeChild(tr); + } + }, + attributes: [ + 'align', + {prop: 'ch', attr: 'char'}, + {prop: 'chOff', attr: 'charoff'}, + {prop: 'span', type: 'long'}, + 'vAlign', + 'width', + ] +}); + +define('HTMLTableRowElement', { + tagName: 'TR', + proto: { + get cells() { + if (!this._cells) { + this._cells = new core.HTMLCollection(this, core.mapper(this, function(n) { + return n.nodeName === 'TD' || n.nodeName === 'TH'; + }, false)); + } + return this._cells; + }, + get rowIndex() { + var table = closest(this, 'TABLE'); + return table ? table.rows._toArray().indexOf(this) : -1; + }, + + get sectionRowIndex() { + return this._parentNode.rows._toArray().indexOf(this); + }, + insertCell: function(index) { + var td = this._ownerDocument.createElement('TD'); + var cells = this.cells._toArray(); + if (index < -1 || index > cells.length) { + throw new core.DOMException(core.INDEX_SIZE_ERR); + } + if (index === -1 || index === cells.length) { + this.appendChild(td); + } + else { + var ref = cells[index]; + this.insertBefore(td, ref); + } + return td; + }, + deleteCell: function(index) { + var cells = this.cells._toArray(); + if (index === -1) { + index = cells.length-1; + } + if (index < 0 || index >= cells.length) { + throw new core.DOMException(core.INDEX_SIZE_ERR); + } + var td = this.cells[index]; + this.removeChild(td); + } + }, + attributes: [ + 'align', + 'bgColor', + {prop: 'ch', attr: 'char'}, + {prop: 'chOff', attr: 'charoff'}, + 'vAlign' + ] +}); + +define('HTMLTableCellElement', { + tagNames: ['TH','TD'], + proto: { + _headers: null, + set headers(h) { + if (h === '') { + //Handle resetting headers so the dynamic getter returns a query + this._headers = null; + return; + } + if (!(h instanceof Array)) { + h = [h]; + } + this._headers = h; + }, + get headers() { + if (this._headers) { + return this._headers.join(' '); + } + var cellIndex = this.cellIndex, + headings = [], + siblings = this._parentNode.getElementsByTagName(this.tagName); + + for (var i=0; i<siblings.length; i++) { + if (siblings.item(i).cellIndex >= cellIndex) { + break; + } + headings.push(siblings.item(i).id); + } + this._headers = headings; + return headings.join(' '); + }, + get cellIndex() { + return closest(this, 'TR').cells._toArray().indexOf(this); + } + }, + attributes: [ + 'abbr', + 'align', + 'axis', + 'bgColor', + {prop: 'ch', attr: 'char'}, + {prop: 'chOff', attr: 'charoff'}, + {prop: 'colSpan', type: 'long'}, + 'height', + {prop: 'noWrap', type: 'boolean'}, + {prop: 'rowSpan', type: 'long'}, + 'scope', + 'vAlign', + 'width' + ] +}); + +define('HTMLFrameSetElement', { + tagName: 'FRAMESET', + attributes: [ + 'cols', + 'rows' + ] +}); + +function loadFrame (frame) { + if (frame._contentDocument) { + // We don't want to access document.parentWindow, since the getter will + // cause a new window to be allocated if it doesn't exist. Probe the + // private variable instead. + if (frame._contentDocument._parentWindow) { + // close calls delete on its document. + frame._contentDocument.parentWindow.close(); + } else { + delete frame._contentDocument; + } + } + + var src = frame.src.trim() === '' ? 'about:blank' : frame.src; + var parentDoc = frame._ownerDocument; + + // If the URL can't be resolved or the src attribute is missing / blank, + // then url should be set to the string "about:blank". + // (http://www.whatwg.org/specs/web-apps/current-work/#the-iframe-element) + var url = core.resourceLoader.resolve(parentDoc, src); + var contentDoc = frame._contentDocument = new core.HTMLDocument({ + parsingMode: 'html', + url: url, + documentRoot: Path.dirname(url) + }); + applyDocumentFeatures(contentDoc, parentDoc.implementation._features); + + var parent = parentDoc.parentWindow; + var contentWindow = contentDoc.parentWindow; + contentWindow.parent = parent; + contentWindow.top = parent.top; + + // Handle about:blank with a simulated load of an empty document. + if(url === 'about:blank') { + core.resourceLoader.enqueue(frame, function() { + contentDoc.write(); + contentDoc.close(); + })(); + } else { + core.resourceLoader.load(frame, url, function(html, filename) { + contentDoc.write(html); + contentDoc.close(); + }); + } +} + +define('HTMLFrameElement', { + tagName: 'FRAME', + init : function () { + // Set up the frames array. window.frames really just returns a reference + // to the window object, so the frames array is just implemented as indexes + // on the window. + var parent = this._ownerDocument.parentWindow; + var frameID = parent._length++; + var self = this; + defineGetter(parent, frameID, function () { + return self.contentWindow; + }); + + // The contentDocument/contentWindow shouldn't be created until the frame + // is inserted: + // "When an iframe element is first inserted into a document, the user + // agent must create a nested browsing context, and then process the + // iframe attributes for the first time." + // (http://www.whatwg.org/specs/web-apps/current-work/#the-iframe-element) + this._initInsertListener = function () { + loadFrame(self); + }; + this.addEventListener('DOMNodeInsertedIntoDocument', this._initInsertListener, false); + }, + proto: { + _attrModified: function(name, value, oldVal) { + core.HTMLElement.prototype._attrModified.call(this, name, value, oldVal); + var self = this; + if (name === 'name') { + // Remove named frame access. + if (oldVal) { + this._ownerDocument.parentWindow._frame(oldVal); + } + // Set up named frame access. + if (value) { + this._ownerDocument.parentWindow._frame(value, this); + } + } else if (name === 'src') { + // Page we don't fetch the page until the node is inserted. This at + // least seems to be the way Chrome does it. + if (!this._attachedToDocument) { + if (!this._waitingOnInsert) { + // First, remove the listener added in 'init'. + this.removeEventListener('DOMNodeInsertedIntoDocument', + this._initInsertListener, false) + + // If we aren't already waiting on an insert, add a listener. + // This guards against src being set multiple times before the frame + // is inserted into the document - we don't want to register multiple + // callbacks. + this.addEventListener('DOMNodeInsertedIntoDocument', function loader () { + self.removeEventListener('DOMNodeInsertedIntoDocument', loader, false); + this._waitingOnInsert = false; + loadFrame(self); + }, false); + this._waitingOnInsert = true; + } + } else { + loadFrame(self); + } + } + }, + _contentDocument : null, + get contentDocument() { + if (this._contentDocument == null) { + this._contentDocument = new core.HTMLDocument({ parsingMode: "html" }); + } + return this._contentDocument; + }, + get contentWindow() { + return this.contentDocument.parentWindow; + } + }, + attributes: [ + 'frameBorder', + 'longDesc', + 'marginHeight', + 'marginWidth', + 'name', + {prop: 'noResize', type: 'boolean'}, + 'scrolling', + {prop: 'src', type: 'string', write: false} + ] +}); + +define('HTMLIFrameElement', { + tagName: 'IFRAME', + parentClass: core.HTMLFrameElement, + attributes: [ + 'align', + 'frameBorder', + 'height', + 'longDesc', + 'marginHeight', + 'marginWidth', + 'name', + 'scrolling', + 'src', + 'width' + ] +}); diff --git a/node_modules/jsdom/lib/jsdom/level2/languages/javascript.js b/node_modules/jsdom/lib/jsdom/level2/languages/javascript.js new file mode 100644 index 0000000..308724d --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level2/languages/javascript.js @@ -0,0 +1,13 @@ +exports.javascript = function(element, code, filename) { + var doc = element.ownerDocument, window = doc && doc.parentWindow; + if (window) { + try { + window.run(code, filename); + } catch (e) { + element.raise( + 'error', 'Running ' + filename + ' failed.', + {error: e, filename: filename} + ); + } + } +}; diff --git a/node_modules/jsdom/lib/jsdom/level2/style.js b/node_modules/jsdom/lib/jsdom/level2/style.js new file mode 100644 index 0000000..ab63fcc --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level2/style.js @@ -0,0 +1,263 @@ +var core = require("../level1/core"), + utils = require("../utils"), + defineGetter = utils.defineGetter, + defineSetter = utils.defineSetter, + inheritFrom = utils.inheritFrom, + cssom = require("cssom"), + cssstyle = require("cssstyle"), + assert = require('assert'); + +// What works now: +// - Accessing the rules defined in individual stylesheets +// - Modifications to style content attribute are reflected in style property +// - Modifications to style property are reflected in style content attribute +// TODO +// - Modifications to style element's textContent are reflected in sheet property. +// - Modifications to style element's sheet property are reflected in textContent. +// - Modifications to link.href property are reflected in sheet property. +// - Less-used features of link: disabled +// - Less-used features of style: disabled, scoped, title +// - CSSOM-View +// - getComputedStyle(): requires default stylesheet, cascading, inheritance, +// filtering by @media (screen? print?), layout for widths/heights +// - Load events are not in the specs, but apparently some browsers +// implement something. Should onload only fire after all @imports have been +// loaded, or only the primary sheet? + +core.StyleSheet = cssom.StyleSheet; +core.MediaList = cssom.MediaList; +core.CSSStyleSheet = cssom.CSSStyleSheet; +core.CSSRule = cssom.CSSRule; +core.CSSStyleRule = cssom.CSSStyleRule; +core.CSSMediaRule = cssom.CSSMediaRule; +core.CSSImportRule = cssom.CSSImportRule; +core.CSSStyleDeclaration = cssstyle.CSSStyleDeclaration; + +// Relavant specs +// http://www.w3.org/TR/DOM-Level-2-Style (2000) +// http://www.w3.org/TR/cssom-view/ (2008) +// http://dev.w3.org/csswg/cssom/ (2010) Meant to replace DOM Level 2 Style +// http://www.whatwg.org/specs/web-apps/current-work/multipage/ HTML5, of course +// http://dev.w3.org/csswg/css-style-attr/ not sure what's new here + +// Objects that aren't in cssom library but should be: +// CSSRuleList (cssom just uses array) +// CSSFontFaceRule +// CSSPageRule + +// These rules don't really make sense to implement, so CSSOM draft makes them +// obsolete. +// CSSCharsetRule +// CSSUnknownRule + +// These objects are considered obsolete by CSSOM draft, although modern +// browsers implement them. +// CSSValue +// CSSPrimitiveValue +// CSSValueList +// RGBColor +// Rect +// Counter + +// StyleSheetList - +// http://www.w3.org/TR/DOM-Level-2-Style/stylesheets.html#StyleSheets-StyleSheetList +// added a push method to help manage the length +core.StyleSheetList = function() { + this._length = 0; +}; +core.StyleSheetList.prototype = { + item: function (i) { + return this[i]; + }, + push: function (sheet) { + this[this._length] = sheet; + this._length++; + }, + get length() { + return this._length; + } +}; + +defineGetter(core.Document.prototype, 'styleSheets', function() { + if (!this._styleSheets) { + this._styleSheets = new core.StyleSheetList(); + } + // TODO: each style and link element should register its sheet on creation + // and remove it on removal. + return this._styleSheets; +}); + + +/** + * @this {core.HTMLLinkElement|core.HTMLStyleElement} + * @param {string} url + * @param {cssom.CSSStyleSheet} sheet + * @see http://dev.w3.org/csswg/cssom/#requirements-on-user-agents-implementing0 + */ +function fetchStylesheet(url, sheet) { + core.resourceLoader.load(this, url, function(data, filename) { + // TODO: abort if the content-type is not text/css, and the document is + // in strict mode + sheet.href = core.resourceLoader.resolve(this.ownerDocument, url); + evaluateStylesheet.call(this, data, sheet, url); + }); +} +/** + * @this {core.HTMLLinkElement|core.HTMLStyleElement} + * @param {string} data + * @param {cssom.CSSStyleSheet} sheet + * @param {string} baseUrl + */ +function evaluateStylesheet(data, sheet, baseUrl) { + // this is the element + var newStyleSheet = cssom.parse(data); + var spliceArgs = newStyleSheet.cssRules; + spliceArgs.unshift(0, sheet.cssRules.length); + Array.prototype.splice.apply(sheet.cssRules, spliceArgs); + scanForImportRules.call(this, sheet.cssRules, baseUrl); + this.ownerDocument.styleSheets.push(sheet); +} +/** + * @this {core.HTMLLinkElement|core.HTMLStyleElement} + * @param {cssom.CSSStyleSheet} sheet + * @param {string} baseUrl + */ +function scanForImportRules(cssRules, baseUrl) { + if (!cssRules) return; + for (var i = 0; i < cssRules.length; ++i) { + if (cssRules[i].cssRules) { + // @media rule: keep searching inside it. + scanForImportRules.call(this, cssRules[i].cssRules, baseUrl); + } else if (cssRules[i].href) { + // @import rule: fetch the resource and evaluate it. + // See http://dev.w3.org/csswg/cssom/#css-import-rule + // If loading of the style sheet fails its cssRules list is simply + // empty. I.e. an @import rule always has an associated style sheet. + fetchStylesheet.call(this, cssRules[i].href, this.sheet); + } + } +} + +/** + * @param {string} data + * @param {cssstyle.CSSStyleDeclaration} style + */ +function evaluateStyleAttribute(data) { + // this is the element. + +} + +/** + * Subclass of core.Attr that reflects the current cssText. + */ +function StyleAttr(node, value) { + this._node = node; + core.Attr.call(this, node.ownerDocument, 'style'); + if (!this._node._ignoreValueOfStyleAttr) { + this.nodeValue = value; + } +} +inheritFrom(core.Attr, StyleAttr, { + get nodeValue() { + if (typeof this._node._style === 'string') { + return this._node._style; + } else { + return this._node.style.cssText; + } + }, + set nodeValue(value) { + this._node._style = value; + } +}); + +var $setNode_super = core.AttributeList.prototype.$setNode; +/** + * Overwrite core.AttrNodeMap#setNamedItem to create a StyleAttr instance + * instead of a core.Attr if the name equals 'style'. + */ +core.AttributeList.prototype.$setNode = function(attr) { + if (attr.name == 'style') { + attr = new StyleAttr(this._parentNode, attr.nodeValue); + } + return $setNode_super.call(this, attr); +}; + +/** + * Lazily create a CSSStyleDeclaration. + */ +defineGetter(core.HTMLElement.prototype, 'style', function() { + if (typeof this._style === 'string') { + // currently, cssom's parse doesn't really work if you pass in + // {state: 'name'}, so instead we just build a dummy sheet. + var styleSheet = cssom.parse('dummy{' + this._style + '}'); + this._style = new cssstyle.CSSStyleDeclaration(); + if (styleSheet.cssRules.length > 0 && styleSheet.cssRules[0].style) { + var newStyle = styleSheet.cssRules[0].style; + for (var i = 0; i < newStyle.length; ++i) { + var prop = newStyle[i]; + this._style.setProperty( + prop, + newStyle.getPropertyValue(prop), + newStyle.getPropertyPriority(prop)); + } + } + } + if (!this._style) { + this._style = new cssstyle.CSSStyleDeclaration(); + + } + if (!this.getAttributeNode('style')) { + // Tell the StyleAttr constructor to not overwrite this._style + this._ignoreValueOfStyleAttr = true; + this.setAttribute('style'); + this._ignoreValueOfStyleAttr = false; + } + return this._style; +}); + +assert.equal(undefined, core.HTMLLinkElement._init); +core.HTMLLinkElement._init = function() { + this.addEventListener('DOMNodeInsertedIntoDocument', function() { + if (!/(?:[ \t\n\r\f]|^)stylesheet(?:[ \t\n\r\f]|$)/i.test(this.rel)) { + // rel is a space-separated list of tokens, and the original rel types + // are case-insensitive. + return; + } + if (this.href) { + fetchStylesheet.call(this, this.href, this.sheet); + } + }); + this.addEventListener('DOMNodeRemovedFromDocument', function() { + }); +}; +/** + * @this {HTMLStyleElement|HTMLLinkElement} + */ +var getOrCreateSheet = function() { + if (!this._cssStyleSheet) { + this._cssStyleSheet = new cssom.CSSStyleSheet(); + } + return this._cssStyleSheet; +}; +defineGetter(core.HTMLLinkElement.prototype, 'sheet', getOrCreateSheet); + +assert.equal(undefined, core.HTMLStyleElement._init); +core.HTMLStyleElement._init = function() { + //console.log('init style') + this.addEventListener('DOMNodeInsertedIntoDocument', function() { + //console.log('style inserted') + //console.log('sheet: ', this.sheet); + if (this.type && this.type !== 'text/css') { + //console.log('bad type: ' + this.type) + return; + } + var content = ''; + this._childNodes.forEach(function (child) { + if (child.nodeType === child.TEXT_NODE) { // text node + content += child.nodeValue; + } + }); + evaluateStylesheet.call(this, content, this.sheet, this._ownerDocument.URL); + }); +}; +defineGetter(core.HTMLStyleElement.prototype, 'sheet', getOrCreateSheet); diff --git a/node_modules/jsdom/lib/jsdom/level3/core.js b/node_modules/jsdom/lib/jsdom/level3/core.js new file mode 100644 index 0000000..0d0820e --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level3/core.js @@ -0,0 +1,597 @@ +var core = require("../level1/core"), + defineGetter = require('../utils').defineGetter, + defineSetter = require('../utils').defineSetter, + HtmlToDom = require('../browser/htmltodom').HtmlToDom, + domToHtml = require('../browser/domtohtml').domToHtml; + +/* + valuetype DOMString sequence<unsigned short>; + typedef unsigned long long DOMTimeStamp; + typedef any DOMUserData; + typedef Object DOMObject; + +*/ +// ExceptionCode +core.VALIDATION_ERR = 16; +core.TYPE_MISMATCH_ERR = 17; + +/* + // Introduced in DOM Level 3: + interface NameList { + DOMString getName(in unsigned long index); + DOMString getNamespaceURI(in unsigned long index); + readonly attribute unsigned long length; + boolean contains(in DOMString str); + boolean containsNS(in DOMString namespaceURI, + in DOMString name); + }; + + // Introduced in DOM Level 3: + interface DOMImplementationList { + DOMImplementation item(in unsigned long index); + readonly attribute unsigned long length; + }; + + // Introduced in DOM Level 3: + interface DOMImplementationSource { + DOMImplementation getDOMImplementation(in DOMString features); + DOMImplementationList getDOMImplementationList(in DOMString features); + }; +*/ + + +core.DOMImplementation.prototype.getFeature = function(feature, version) { + +}; + +/* + interface Node { + // Modified in DOM Level 3: + Node insertBefore(in Node newChild, + in Node refChild) + raises(DOMException); + // Modified in DOM Level 3: + Node replaceChild(in Node newChild, + in Node oldChild) + raises(DOMException); + // Modified in DOM Level 3: + Node removeChild(in Node oldChild) + raises(DOMException); + // Modified in DOM Level 3: + Node appendChild(in Node newChild) + raises(DOMException); + boolean hasChildNodes(); + Node cloneNode(in boolean deep); + // Modified in DOM Level 3: + void normalize(); + // Introduced in DOM Level 3: + readonly attribute DOMString baseURI; +*/ + +// Compare Document Position +var DOCUMENT_POSITION_DISCONNECTED = core.Node.DOCUMENT_POSITION_DISCONNECTED = + core.Node.prototype.DOCUMENT_POSITION_DISCONNECTED = 0x01; + +var DOCUMENT_POSITION_PRECEDING = core.Node.DOCUMENT_POSITION_PRECEDING = + core.Node.prototype.DOCUMENT_POSITION_PRECEDING = 0x02; + +var DOCUMENT_POSITION_FOLLOWING = core.Node.DOCUMENT_POSITION_FOLLOWING = + core.Node.prototype.DOCUMENT_POSITION_FOLLOWING = 0x04; + +var DOCUMENT_POSITION_CONTAINS = core.Node.DOCUMENT_POSITION_CONTAINS = + core.Node.prototype.DOCUMENT_POSITION_CONTAINS = 0x08; + +var DOCUMENT_POSITION_CONTAINED_BY = core.Node.DOCUMENT_POSITION_CONTAINED_BY = + core.Node.prototype.DOCUMENT_POSITION_CONTAINED_BY = 0x10; + +var DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = core.Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = + core.Node.prototype.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; + +var DOCUMENT_TYPE_NODE = core.Node.prototype.DOCUMENT_TYPE_NODE; + +core.Node.prototype.compareDocumentPosition = function compareDocumentPosition( otherNode ) { + if( !(otherNode instanceof core.Node) ) { + throw Error("Comparing position against non-Node values is not allowed") + } + var thisOwner, otherOwner; + + if( this.nodeType === this.DOCUMENT_NODE) + thisOwner = this + else + thisOwner = this.ownerDocument + + if( otherNode.nodeType === this.DOCUMENT_NODE) + otherOwner = otherNode + else + otherOwner = otherNode.ownerDocument + + if( this === otherNode ) return 0 + if( this === otherNode.ownerDocument ) return DOCUMENT_POSITION_FOLLOWING + DOCUMENT_POSITION_CONTAINED_BY + if( this.ownerDocument === otherNode ) return DOCUMENT_POSITION_PRECEDING + DOCUMENT_POSITION_CONTAINS + if( thisOwner !== otherOwner ) return DOCUMENT_POSITION_DISCONNECTED + + // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child. + if( this.nodeType === this.ATTRIBUTE_NODE && this._childNodes && this._childNodes.indexOf(otherNode) !== -1) + return DOCUMENT_POSITION_FOLLOWING + DOCUMENT_POSITION_CONTAINED_BY + + if( otherNode.nodeType === this.ATTRIBUTE_NODE && otherNode._childNodes && otherNode._childNodes.indexOf(this) !== -1) + return DOCUMENT_POSITION_PRECEDING + DOCUMENT_POSITION_CONTAINS + + var point = this + var parents = [ ] + var previous = null + while( point ) { + if( point == otherNode ) return DOCUMENT_POSITION_PRECEDING + DOCUMENT_POSITION_CONTAINS + parents.push( point ) + point = point._parentNode + } + point = otherNode + previous = null + while( point ) { + if( point == this ) return DOCUMENT_POSITION_FOLLOWING + DOCUMENT_POSITION_CONTAINED_BY + var location_index = parents.indexOf( point ) + if( location_index !== -1) { + var smallest_common_ancestor = parents[ location_index ] + var this_index = smallest_common_ancestor._childNodes.indexOf( parents[location_index - 1] ) + var other_index = smallest_common_ancestor._childNodes.indexOf( previous ) + if( this_index > other_index ) { + return DOCUMENT_POSITION_PRECEDING + } + else { + return DOCUMENT_POSITION_FOLLOWING + } + } + previous = point + point = point._parentNode + } + return DOCUMENT_POSITION_DISCONNECTED +}; + +// @see http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#Node3-textContent +defineGetter(core.Node.prototype, 'textContent', function() { + switch (this.nodeType) { + case this.COMMENT_NODE: + case this.CDATA_SECTION_NODE: + case this.PROCESSING_INSTRUCTION_NODE: + case this.TEXT_NODE: + return this.nodeValue; + + case this.ATTRIBUTE_NODE: + case this.DOCUMENT_FRAGMENT_NODE: + case this.ELEMENT_NODE: + var out = ''; + for (var i = 0 ; i < this._childNodes.length ; ++i) { + if (this._childNodes[i].nodeType !== this.COMMENT_NODE && + this._childNodes[i].nodeType !== this.PROCESSING_INSTRUCTION_NODE) { + out += this._childNodes[i].textContent || ''; + } + } + return out; + + default: + return null; + } +}); + +defineSetter(core.Node.prototype, 'textContent', function(txt) { + switch (this.nodeType) { + case this.COMMENT_NODE: + case this.CDATA_SECTION_NODE: + case this.PROCESSING_INSTRUCTION_NODE: + case this.TEXT_NODE: + return this.nodeValue = String(txt); + } + + for (var i = this._childNodes.length; --i >=0;) { + this.removeChild(this._childNodes[i]); + } + if (txt !== "" && txt != null) { + this.appendChild(this._ownerDocument.createTextNode(txt)); + } + return txt; +}); + +/* + // Introduced in DOM Level 3: + DOMString lookupPrefix(in DOMString namespaceURI); + // Introduced in DOM Level 3: + boolean isDefaultNamespace(in DOMString namespaceURI); + // Introduced in DOM Level 3: + DOMString lookupNamespaceURI(in DOMString prefix); + // Introduced in DOM Level 3: + DOMObject getFeature(in DOMString feature, + in DOMString version); +*/ +// Introduced in DOM Level 3: +core.Node.prototype.setUserData = function(key, data, handler) { + var r = this[key] || null; + this[key] = data; + return(r); +}; + +// Introduced in DOM Level 3: +core.Node.prototype.getUserData = function(key) { + var r = this[key] || null; + return(r); +}; +/* + interface NodeList { + Node item(in unsigned long index); + readonly attribute unsigned long length; + }; + + interface NamedNodeMap { + Node getNamedItem(in DOMString name); + Node setNamedItem(in Node arg) + raises(DOMException); + Node removeNamedItem(in DOMString name) + raises(DOMException); + Node item(in unsigned long index); + readonly attribute unsigned long length; + // Introduced in DOM Level 2: + Node getNamedItemNS(in DOMString namespaceURI, + in DOMString localName) + raises(DOMException); + // Introduced in DOM Level 2: + Node setNamedItemNS(in Node arg) + raises(DOMException); + // Introduced in DOM Level 2: + Node removeNamedItemNS(in DOMString namespaceURI, + in DOMString localName) + raises(DOMException); + }; + + interface CharacterData : Node { + attribute DOMString data; + // raises(DOMException) on setting + // raises(DOMException) on retrieval + + readonly attribute unsigned long length; + DOMString substringData(in unsigned long offset, + in unsigned long count) + raises(DOMException); + void appendData(in DOMString arg) + raises(DOMException); + void insertData(in unsigned long offset, + in DOMString arg) + raises(DOMException); + void deleteData(in unsigned long offset, + in unsigned long count) + raises(DOMException); + void replaceData(in unsigned long offset, + in unsigned long count, + in DOMString arg) + raises(DOMException); + }; + + interface Attr : Node { + readonly attribute DOMString name; + readonly attribute boolean specified; + attribute DOMString value; + // raises(DOMException) on setting + + // Introduced in DOM Level 2: + readonly attribute Element ownerElement; + // Introduced in DOM Level 3: + readonly attribute TypeInfo schemaTypeInfo; + +*/ + // Introduced in DOM Level 3: +defineGetter(core.Attr.prototype, 'isId', function() { + return (this.name.toLowerCase() === 'id'); +}); +/* + }; + + interface Element : Node { + readonly attribute DOMString tagName; + DOMString getAttribute(in DOMString name); + void setAttribute(in DOMString name, + in DOMString value) + raises(DOMException); + void removeAttribute(in DOMString name) + raises(DOMException); + Attr getAttributeNode(in DOMString name); + Attr setAttributeNode(in Attr newAttr) + raises(DOMException); + Attr removeAttributeNode(in Attr oldAttr) + raises(DOMException); + NodeList getElementsByTagName(in DOMString name); + // Introduced in DOM Level 2: + DOMString getAttributeNS(in DOMString namespaceURI, + in DOMString localName) + raises(DOMException); + // Introduced in DOM Level 2: + void setAttributeNS(in DOMString namespaceURI, + in DOMString qualifiedName, + in DOMString value) + raises(DOMException); + // Introduced in DOM Level 2: + void removeAttributeNS(in DOMString namespaceURI, + in DOMString localName) + raises(DOMException); + // Introduced in DOM Level 2: + Attr getAttributeNodeNS(in DOMString namespaceURI, + in DOMString localName) + raises(DOMException); + // Introduced in DOM Level 2: + Attr setAttributeNodeNS(in Attr newAttr) + raises(DOMException); + // Introduced in DOM Level 2: + NodeList getElementsByTagNameNS(in DOMString namespaceURI, + in DOMString localName) + raises(DOMException); + // Introduced in DOM Level 2: + boolean hasAttribute(in DOMString name); + // Introduced in DOM Level 2: + boolean hasAttributeNS(in DOMString namespaceURI, + in DOMString localName) + raises(DOMException); + // Introduced in DOM Level 3: + readonly attribute TypeInfo schemaTypeInfo; + // Introduced in DOM Level 3: + void setIdAttribute(in DOMString name, + in boolean isId) + raises(DOMException); + // Introduced in DOM Level 3: + void setIdAttributeNS(in DOMString namespaceURI, + in DOMString localName, + in boolean isId) + raises(DOMException); + // Introduced in DOM Level 3: + void setIdAttributeNode(in Attr idAttr, + in boolean isId) + raises(DOMException); + }; + + interface Text : CharacterData { + Text splitText(in unsigned long offset) + raises(DOMException); + // Introduced in DOM Level 3: + readonly attribute boolean isElementContentWhitespace; + // Introduced in DOM Level 3: + readonly attribute DOMString wholeText; + // Introduced in DOM Level 3: + Text replaceWholeText(in DOMString content) + raises(DOMException); + }; + + interface Comment : CharacterData { + }; + + // Introduced in DOM Level 3: + interface TypeInfo { + readonly attribute DOMString typeName; + readonly attribute DOMString typeNamespace; + + // DerivationMethods + const unsigned long DERIVATION_RESTRICTION = 0x00000001; + const unsigned long DERIVATION_EXTENSION = 0x00000002; + const unsigned long DERIVATION_UNION = 0x00000004; + const unsigned long DERIVATION_LIST = 0x00000008; + + boolean isDerivedFrom(in DOMString typeNamespaceArg, + in DOMString typeNameArg, + in unsigned long derivationMethod); + }; +*/ +// Introduced in DOM Level 3: +core.UserDataHandler = function() {}; +core.UserDataHandler.prototype.NODE_CLONED = 1; +core.UserDataHandler.prototype.NODE_IMPORTED = 2; +core.UserDataHandler.prototype.NODE_DELETED = 3; +core.UserDataHandler.prototype.NODE_RENAMED = 4; +core.UserDataHandler.prototype.NODE_ADOPTED = 5; +core.UserDataHandler.prototype.handle = function(operation, key, data, src, dst) {}; + +// Introduced in DOM Level 3: +core.DOMError = function(severity, message, type, relatedException, relatedData, location) { + this._severity = severity; + this._message = message; + this._type = type; + this._relatedException = relatedException; + this._relatedData = relatedData; + this._location = location; +}; +core.DOMError.prototype = {}; +core.DOMError.prototype.SEVERITY_WARNING = 1; +core.DOMError.prototype.SEVERITY_ERROR = 2; +core.DOMError.prototype.SEVERITY_FATAL_ERROR = 3; +defineGetter(core.DOMError.prototype, 'severity', function() { + return this._severity; +}); +defineGetter(core.DOMError.prototype, 'message', function() { + return this._message; +}); +defineGetter(core.DOMError.prototype, 'type', function() { + return this._type; +}); +defineGetter(core.DOMError.prototype, 'relatedException', function() { + return this._relatedException; +}); +defineGetter(core.DOMError.prototype, 'relatedData', function() { + return this._relatedData; +}); +defineGetter(core.DOMError.prototype, 'location', function() { + return this._location; +}); + +/* + // Introduced in DOM Level 3: + interface DOMErrorHandler { + boolean handleError(in DOMError error); + }; + + // Introduced in DOM Level 3: + interface DOMLocator { + readonly attribute long lineNumber; + readonly attribute long columnNumber; + readonly attribute long byteOffset; + readonly attribute long utf16Offset; + readonly attribute Node relatedNode; + readonly attribute DOMString uri; + }; +*/ + +// Introduced in DOM Level 3: +core.DOMConfiguration = function(){ + var possibleParameterNames = { + 'canonical-form': [false, true], // extra rules for true + 'cdata-sections': [true, false], + 'check-character-normalization': [false, true], + 'comments': [true, false], + 'datatype-normalization': [false, true], + 'element-content-whitespace': [true, false], + 'entities': [true, false], + // 'error-handler': [], + 'infoset': [undefined, true, false], // extra rules for true + 'namespaces': [true, false], + 'namespace-declarations': [true, false], // only checked if namespaces is true + 'normalize-characters': [false, true], + // 'schema-location': [], + // 'schema-type': [], + 'split-cdata-sections': [true, false], + 'validate': [false, true], + 'validate-if-schema': [false, true], + 'well-formed': [true, false] + } +}; + +core.DOMConfiguration.prototype = { + setParameter: function(name, value) {}, + getParameter: function(name) {}, + canSetParameter: function(name, value) {}, + parameterNames: function() {} +}; + +//core.Document.prototype._domConfig = new core.DOMConfiguration(); +defineGetter(core.Document.prototype, 'domConfig', function() { + return this._domConfig || new core.DOMConfiguration();; +}); + +// Introduced in DOM Level 3: +core.DOMStringList = function() {}; + +core.DOMStringList.prototype = { + item: function() {}, + length: function() {}, + contains: function() {} +}; + + +/* + interface CDATASection : Text { + }; + + interface DocumentType : Node { + readonly attribute DOMString name; + readonly attribute NamedNodeMap entities; + readonly attribute NamedNodeMap notations; + // Introduced in DOM Level 2: + readonly attribute DOMString publicId; + // Introduced in DOM Level 2: + readonly attribute DOMString systemId; + // Introduced in DOM Level 2: + readonly attribute DOMString internalSubset; + }; + + interface Notation : Node { + readonly attribute DOMString publicId; + readonly attribute DOMString systemId; + }; + + interface Entity : Node { + readonly attribute DOMString publicId; + readonly attribute DOMString systemId; + readonly attribute DOMString notationName; + // Introduced in DOM Level 3: + readonly attribute DOMString inputEncoding; + // Introduced in DOM Level 3: + readonly attribute DOMString xmlEncoding; + // Introduced in DOM Level 3: + readonly attribute DOMString xmlVersion; + }; + + interface EntityReference : Node { + }; + + interface ProcessingInstruction : Node { + readonly attribute DOMString target; + attribute DOMString data; + // raises(DOMException) on setting + + }; + + interface DocumentFragment : Node { + }; + + interface Document : Node { + // Modified in DOM Level 3: + readonly attribute DocumentType doctype; + readonly attribute DOMImplementation implementation; + readonly attribute Element documentElement; + Element createElement(in DOMString tagName) + raises(DOMException); + DocumentFragment createDocumentFragment(); + Text createTextNode(in DOMString data); + Comment createComment(in DOMString data); + CDATASection createCDATASection(in DOMString data) + raises(DOMException); + ProcessingInstruction createProcessingInstruction(in DOMString target, + in DOMString data) + raises(DOMException); + Attr createAttribute(in DOMString name) + raises(DOMException); + EntityReference createEntityReference(in DOMString name) + raises(DOMException); + NodeList getElementsByTagName(in DOMString tagname); + // Introduced in DOM Level 2: + Node importNode(in Node importedNode, + in boolean deep) + raises(DOMException); + // Introduced in DOM Level 2: + Element createElementNS(in DOMString namespaceURI, + in DOMString qualifiedName) + raises(DOMException); + // Introduced in DOM Level 2: + Attr createAttributeNS(in DOMString namespaceURI, + in DOMString qualifiedName) + raises(DOMException); + // Introduced in DOM Level 2: + NodeList getElementsByTagNameNS(in DOMString namespaceURI, + in DOMString localName); + // Introduced in DOM Level 2: + Element getElementById(in DOMString elementId); +*/ +/* + // Introduced in DOM Level 3: + readonly attribute DOMString xmlEncoding; + // Introduced in DOM Level 3: + attribute boolean xmlStandalone; + // raises(DOMException) on setting + + // Introduced in DOM Level 3: + attribute DOMString xmlVersion; + // raises(DOMException) on setting + + // Introduced in DOM Level 3: + attribute boolean strictErrorChecking; + // Introduced in DOM Level 3: + attribute DOMString documentURI; + // Introduced in DOM Level 3: + Node adoptNode(in Node source) + raises(DOMException); + // Introduced in DOM Level 3: + readonly attribute DOMConfiguration domConfig; + // Introduced in DOM Level 3: + void normalizeDocument(); + // Introduced in DOM Level 3: + Node renameNode(in Node n, + in DOMString namespaceURI, + in DOMString qualifiedName) + raises(DOMException); + }; +}; + +#endif // _DOM_IDL_ +*/ diff --git a/node_modules/jsdom/lib/jsdom/level3/ls.js b/node_modules/jsdom/lib/jsdom/level3/ls.js new file mode 100644 index 0000000..5cc44f4 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level3/ls.js @@ -0,0 +1,211 @@ +// w3c Load/Save functionality: http://www.w3.org/TR/2004/REC-DOM-Level-3-LS-20040407/ + +var core = require('../level1/core'); +var createFrom = require('../utils').createFrom; + +var ls = {}; + +// TODO: what is this? +//typedef dom::DOMConfiguration DOMConfiguration; + +ls.LSException = function LSException(code) { + this.code = code; +}; + +ls.LSException.prototype = { + // LSExceptionCode + PARSE_ERR : 81, + SERIALIZE_ERR : 82 +}; + +ls.DOMImplementationLS = function DOMImplementationLS() { + +}; + +var DOMImplementationExtension = { + + // DOMImplementationLSMode + MODE_SYNCHRONOUS : 1, + MODE_ASYNCHRONOUS : 2, + + // raises(dom::DOMException); + createLSParser : function(/* int */ mode, /* string */ schemaType) { + return new ls.LSParser(mode, schemaType); + }, + + createLSSerializer : function() { + return new ls.LSSerializer(); + }, + + createLSInput : function() { + return new ls.LSInput(); + }, + + createLSOutput : function() { + return new ls.LSOutput(); + } +}; + +Object.keys(DOMImplementationExtension).forEach(function(k, v) { + core.DOMImplementation.prototype[k] = DOMImplementationExtension[k]; +}); + +ls.DOMImplementationLS.prototype = DOMImplementationExtension; + +core.Document.getFeature = function() { + return DOMImplementationExtension; +}; + +ls.LSParser = function LSParser() { + this._domConfig = new core.DOMConfiguration(); +}; +ls.LSParser.prototype = { + get domConfig() { return this._domConfig; }, + get filter() { return this._filter || null; }, + set filter(value) { this._filter = value; }, + get async() { return this._async; }, + get busy() { return this._busy; }, + + // raises(dom::DOMException, LSException); + parse : function (/* LSInput */ input) { + var doc = new core.Document(); + doc._inputEncoding = 'UTF-16'; + return doc; + }, + + // raises(dom::DOMException, LSException); + parseURI : function(/* string */ uri) { + return new core.Document(); + }, + + // ACTION_TYPES + ACTION_APPEND_AS_CHILDREN : 1, + ACTION_REPLACE_CHILDREN : 2, + ACTION_INSERT_BEFORE : 3, + ACTION_INSERT_AFTER : 4, + ACTION_REPLACE : 5, + + // @returns Node + // @raises DOMException, LSException + parseWithContext : function(/* LSInput */ input, /* Node */ contextArg, /* int */ action) { + return new core.Node(); + }, + + abort : function() { + // TODO: implement + } +}; + +ls.LSInput = function LSInput() {}; +ls.LSInput.prototype = { + get characterStream() { return this._characterStream || null; }, + set characterStream(value) { this._characterStream = value; }, + get byteStream() { return this._byteStream || null; }, + set byteStream(value) { this._byteStream = value; }, + get stringData() { return this._stringData || null; }, + set stringData(value) { this._stringData = value; }, + get systemId() { return this._systemId || null; }, + set systemId(value) { this._systemId = value; }, + get publicId() { return this._publicId || null; }, + set publicId(value) { this._publicId = value; }, + get baseURI() { return this._baseURI || null; }, + set baseURI(value) { this._baseURI = value; }, + get encoding() { return this._encoding || null; }, + set encoding(value) { this._encoding = value; }, + get certifiedText() { return this._certifiedText || null; }, + set certifiedText(value) { this._certifiedText = value; }, +}; + +ls.LSResourceResolver = function LSResourceResolver() {}; + +// @returns LSInput +ls.LSResourceResolver.prototype.resolveResource = function(type, namespaceURI, publicId, systemId, baseURI) { + return new ls.LSInput(); +}; + +ls.LSParserFilter = function LSParserFilter() {}; +ls.LSParserFilter.prototype = { + + // Constants returned by startElement and acceptNode + FILTER_ACCEPT : 1, + FILTER_REJECT : 2, + FILTER_SKIP : 3, + FILTER_INTERRUPT : 4, + + get whatToShow() { return this._whatToShow; }, + + // @returns int + startElement : function(/* Element */ elementArg) { + return 0; + }, + + // @returns int + acceptNode : function(/* Node */ nodeArg) { + return nodeArg; + } +}; + +ls.LSSerializer = function LSSerializer() { + this._domConfig = new core.DOMConfiguration(); +}; +ls.LSSerializer.prototype = { + get domConfig() { return this._domConfig; }, + get newLine() { return this._newLine || null; }, + set newLine(value) { this._newLine = value; }, + get filter() { return this._filter || null; }, + set filter(value) { this._filter = value; }, + + // @returns boolean + // @raises LSException + write : function(/* Node */ nodeArg, /* LSOutput */ destination) { + return true; + }, + + // @returns boolean + // @raises LSException + writeToURI : function(/* Node */ nodeArg, /* string */ uri) { + return true; + }, + + // @returns string + // @raises DOMException, LSException + writeToString : function(/* Node */ nodeArg) { + return ""; + } +}; + +ls.LSOutput = function LSOutput() {}; +ls.LSOutput.prototype = { + get characterStream() { return this._characterStream || null; }, + set characterStream(value) { this._characterStream = value; }, + get byteStream() { return this._byteStream || null; }, + set byteStream(value) { this._byteStream = value; }, + get systemId() { return this._systemId || null; }, + set systemId(value) { this._systemId = value; }, + get encoding() { return this._encoding || null; }, + set encoding(value) { this._encoding = value; }, +}; + +ls.LSProgressEvent = function LSProgressEvent() {}; +ls.LSProgressEvent.prototype = createFrom(core.Event, { + constructor: ls.LSProgressEvent, + get input() { return this._input; }, + get position() { return this._position; }, + get totalSize() { return this._totalSize; }, +}); + +ls.LSLoadEvent = function LSLoadEvent() {}; +ls.LSLoadEvent.prototype = createFrom(core.Event, { + get newDocument() { return this._newDocument; }, + get input() { return this._input; }, +}); + + +// TODO: do traversal +ls.LSSerializerFilter = function LSSerializerFilter() {}; +ls.LSSerializerFilter.prototype = { + get whatToShow() { return this._whatToShow; }, +}; + +// ls.LSSerializerFilter.prototype.__proto__ = level2.traversal.NodeFiler; + diff --git a/node_modules/jsdom/lib/jsdom/level3/xpath.js b/node_modules/jsdom/lib/jsdom/level3/xpath.js new file mode 100644 index 0000000..98e3a7d --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/level3/xpath.js @@ -0,0 +1,1866 @@ +/** Here is yet another implementation of XPath 1.0 in Javascript. + * + * My goal was to make it relatively compact, but as I fixed all the axis bugs + * the axes became more and more complicated. :-(. + * + * I have not implemented namespaces or case-sensitive axes for XML yet. + * + * How to test it in Chrome: You can make a Chrome extension that replaces + * the WebKit XPath parser with this one. But it takes a bit of effort to + * get around isolated world and same-origin restrictions: + * manifest.json: + { + "name": "XPathTest", + "version": "0.1", + "content_scripts": [{ + "matches": ["http://localhost/*"], // or wildcard host + "js": ["xpath.js", "injection.js"], + "all_frames": true, "run_at": "document_start" + }] + } + * injection.js: + // goal: give my xpath object to the website's JS context. + var script = document.createElement('script'); + script.textContent = + "document.addEventListener('xpathextend', function(e) {\n" + + " console.log('extending document with xpath...');\n" + + " e.detail(window);" + + "});"; + document.documentElement.appendChild(script); + document.documentElement.removeChild(script); + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent('xpathextend', true, true, this.xpath.extend); + document.dispatchEvent(evt); + */ +(function() { + var xpath; + var core; + if ('function' === typeof require) { + xpath = exports; // the tests go through this + core = require("../level1/core"); + } else { + xpath = {}; + core = this; + } + + + /*************************************************************************** + * Tokenization * + ***************************************************************************/ + /** + * The XPath lexer is basically a single regular expression, along with + * some helper functions to pop different types. + */ + var Stream = xpath.Stream = function Stream(str) { + this.original = this.str = str; + this.peeked = null; + // TODO: not really needed, but supposedly tokenizer also disambiguates + // a * b vs. node test * + this.prev = null; // for debugging + this.prevprev = null; + } + Stream.prototype = { + peek: function() { + if (this.peeked) return this.peeked; + var m = this.re.exec(this.str); + if (!m) return null; + this.str = this.str.substr(m[0].length); + return this.peeked = m[1]; + }, + /** Peek 2 tokens ahead. */ + peek2: function() { + this.peek(); // make sure this.peeked is set + var m = this.re.exec(this.str); + if (!m) return null; + return m[1]; + }, + pop: function() { + var r = this.peek(); + this.peeked = null; + this.prevprev = this.prev; + this.prev = r; + return r; + }, + trypop: function(tokens) { + var tok = this.peek(); + if (tok === tokens) return this.pop(); + if (Array.isArray(tokens)) { + for (var i = 0; i < tokens.length; ++i) { + var t = tokens[i]; + if (t == tok) return this.pop();; + } + } + }, + trypopfuncname: function() { + var tok = this.peek(); + if (!this.isQnameRe.test(tok)) + return null; + switch (tok) { + case 'comment': case 'text': case 'processing-instruction': case 'node': + return null; + } + if ('(' != this.peek2()) return null; + return this.pop(); + }, + trypopaxisname: function() { + var tok = this.peek(); + switch (tok) { + case 'ancestor': case 'ancestor-or-self': case 'attribute': + case 'child': case 'descendant': case 'descendant-or-self': + case 'following': case 'following-sibling': case 'namespace': + case 'parent': case 'preceding': case 'preceding-sibling': case 'self': + if ('::' == this.peek2()) return this.pop(); + } + return null; + }, + trypopnametest: function() { + var tok = this.peek(); + if ('*' === tok || this.startsWithNcNameRe.test(tok)) return this.pop(); + return null; + }, + trypopliteral: function() { + var tok = this.peek(); + if (null == tok) return null; + var first = tok.charAt(0); + var last = tok.charAt(tok.length - 1); + if ('"' === first && '"' === last || + "'" === first && "'" === last) { + this.pop(); + return tok.substr(1, tok.length - 2); + } + }, + trypopnumber: function() { + var tok = this.peek(); + if (this.isNumberRe.test(tok)) return parseFloat(this.pop()); + else return null; + }, + trypopvarref: function() { + var tok = this.peek(); + if (null == tok) return null; + if ('$' === tok.charAt(0)) return this.pop().substr(1); + else return null; + }, + position: function() { + return this.original.length - this.str.length; + } + }; + (function() { + // http://www.w3.org/TR/REC-xml-names/#NT-NCName + var nameStartCharsExceptColon = + 'A-Z_a-z\xc0-\xd6\xd8-\xf6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF' + + '\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF' + + '\uFDF0-\uFFFD'; // JS doesn't support [#x10000-#xEFFFF] + var nameCharExceptColon = nameStartCharsExceptColon + + '\\-\\.0-9\xb7\u0300-\u036F\u203F-\u2040'; + var ncNameChars = '[' + nameStartCharsExceptColon + + '][' + nameCharExceptColon + ']*' + // http://www.w3.org/TR/REC-xml-names/#NT-QName + var qNameChars = ncNameChars + '(?::' + ncNameChars + ')?'; + var otherChars = '\\.\\.|[\\(\\)\\[\\].@,]|::'; // .. must come before [.] + var operatorChars = + 'and|or|mod|div|' + + '//|!=|<=|>=|[*/|+\\-=<>]'; // //, !=, <=, >= before individual ones. + var literal = '"[^"]*"|' + "'[^']*'"; + var numberChars = '[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+'; + var variableReference = '\\$' + qNameChars; + var nameTestChars = '\\*|' + ncNameChars + ':\\*|' + qNameChars; + var optionalSpace = '[ \t\r\n]*'; // stricter than regexp \s. + var nodeType = 'comment|text|processing-instruction|node'; + var re = new RegExp( + // numberChars before otherChars so that leading-decimal doesn't become . + '^' + optionalSpace + '(' + numberChars + '|' + otherChars + '|' + + nameTestChars + '|' + operatorChars + '|' + literal + '|' + + variableReference + ')' + // operatorName | nodeType | functionName | axisName are lumped into + // qName for now; we'll check them on pop. + ); + Stream.prototype.re = re; + Stream.prototype.startsWithNcNameRe = new RegExp('^' + ncNameChars); + Stream.prototype.isQnameRe = new RegExp('^' + qNameChars + '$'); + Stream.prototype.isNumberRe = new RegExp('^' + numberChars + '$'); + })(); + + /*************************************************************************** + * Parsing * + ***************************************************************************/ + var parse = xpath.parse = function parse(stream, a) { + var r = orExpr(stream,a); + var x, unparsed = []; + while (x = stream.pop()) { + unparsed.push(x); + } + if (unparsed.length) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Unparsed tokens: ' + unparsed.join(' ')); + return r; + } + + /** + * binaryL ::= subExpr + * | binaryL op subExpr + * so a op b op c becomes ((a op b) op c) + */ + function binaryL(subExpr, stream, a, ops) { + var lhs = subExpr(stream, a); + if (lhs == null) return null; + var op; + while (op = stream.trypop(ops)) { + var rhs = subExpr(stream, a); + if (rhs == null) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected something after ' + op); + lhs = a.node(op, lhs, rhs); + } + return lhs; + } + /** + * Too bad this is never used. If they made a ** operator (raise to power), + ( we would use it. + * binaryR ::= subExpr + * | subExpr op binaryR + * so a op b op c becomes (a op (b op c)) + */ + function binaryR(subExpr, stream, a, ops) { + var lhs = subExpr(stream, a); + if (lhs == null) return null; + var op = stream.trypop(ops); + if (op) { + var rhs = binaryR(stream, a); + if (rhs == null) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected something after ' + op); + return a.node(op, lhs, rhs); + } else { + return lhs;// TODO + } + } + /** [1] LocationPath::= RelativeLocationPath | AbsoluteLocationPath + * e.g. a, a/b, //a/b + */ + function locationPath(stream, a) { + return absoluteLocationPath(stream, a) || + relativeLocationPath(null, stream, a); + } + /** [2] AbsoluteLocationPath::= '/' RelativeLocationPath? | AbbreviatedAbsoluteLocationPath + * [10] AbbreviatedAbsoluteLocationPath::= '//' RelativeLocationPath + */ + function absoluteLocationPath(stream, a) { + var op = stream.peek(); + if ('/' === op || '//' === op) { + var lhs = a.node('Root'); + return relativeLocationPath(lhs, stream, a, true); + } else { + return null; + } + } + /** [3] RelativeLocationPath::= Step | RelativeLocationPath '/' Step | + * | AbbreviatedRelativeLocationPath + * [11] AbbreviatedRelativeLocationPath::= RelativeLocationPath '//' Step + * e.g. p/a, etc. + */ + function relativeLocationPath(lhs, stream, a, isOnlyRootOk) { + if (null == lhs) { + lhs = step(stream, a); + if (null == lhs) return lhs; + } + var op; + while (op = stream.trypop(['/', '//'])) { + if ('//' === op) { + lhs = a.node('/', lhs, + a.node('Axis', 'descendant-or-self', 'node', undefined)); + } + var rhs = step(stream, a); + if (null == rhs && '/' === op && isOnlyRootOk) return lhs; + else isOnlyRootOk = false; + if (null == rhs) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected step after ' + op); + lhs = a.node('/', lhs, rhs); + } + return lhs; + } + /** [4] Step::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep + * [12] AbbreviatedStep::= '.' | '..' + * e.g. @href, self::p, p, a[@href], ., .. + */ + function step(stream, a) { + var abbrStep = stream.trypop(['.', '..']); + if ('.' === abbrStep) // A location step of . is short for self::node(). + return a.node('Axis', 'self', 'node'); + if ('..' === abbrStep) // A location step of .. is short for parent::node() + return a.node('Axis', 'parent', 'node'); + + var axis = axisSpecifier(stream, a); + var nodeType = nodeTypeTest(stream, a); + var nodeName; + if (null == nodeType) nodeName = nodeNameTest(stream, a); + if (null == axis && null == nodeType && null == nodeName) return null; + if (null == nodeType && null == nodeName) + throw new XPathException( + XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected nodeTest after axisSpecifier ' + axis); + if (null == axis) axis = 'child'; + if (null == nodeType) { + // When there's only a node name, then the node type is forced to be the + // principal node type of the axis. + // see http://www.w3.org/TR/xpath/#dt-principal-node-type + if ('attribute' === axis) nodeType = 'attribute'; + else if ('namespace' === axis) nodeType = 'namespace'; + else nodeType = 'element'; + } + var lhs = a.node('Axis', axis, nodeType, nodeName); + var pred; + while (null != (pred = predicate(lhs, stream, a))) { + lhs = pred; + } + return lhs; + } + /** [5] AxisSpecifier::= AxisName '::' | AbbreviatedAxisSpecifier + * [6] AxisName::= 'ancestor' | 'ancestor-or-self' | 'attribute' | 'child' + * | 'descendant' | 'descendant-or-self' | 'following' + * | 'following-sibling' | 'namespace' | 'parent' | + * | 'preceding' | 'preceding-sibling' | 'self' + * [13] AbbreviatedAxisSpecifier::= '@'? + */ + function axisSpecifier(stream, a) { + var attr = stream.trypop('@'); + if (null != attr) return 'attribute'; + var axisName = stream.trypopaxisname(); + if (null != axisName) { + var coloncolon = stream.trypop('::'); + if (null == coloncolon) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Should not happen. Should be ::.'); + return axisName; + } + } + /** [7] NodeTest::= NameTest | NodeType '(' ')' | 'processing-instruction' '(' Literal ')' + * [38] NodeType::= 'comment' | 'text' | 'processing-instruction' | 'node' + * I've split nodeTypeTest from nodeNameTest for convenience. + */ + function nodeTypeTest(stream, a) { + if ('(' !== stream.peek2()) { + return null; + } + var type = stream.trypop(['comment', 'text', 'processing-instruction', 'node']); + if (null != type) { + if (null == stream.trypop('(')) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Should not happen.'); + var param = undefined; + if (type == 'processing-instruction') { + param = stream.trypopliteral(); + } + if (null == stream.trypop(')')) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected close parens.'); + return type + } + } + function nodeNameTest(stream, a) { + var name = stream.trypopnametest(); + if (name != null) return name; + else return null; + } + /** [8] Predicate::= '[' PredicateExpr ']' + * [9] PredicateExpr::= Expr + */ + function predicate(lhs, stream, a) { + if (null == stream.trypop('[')) return null; + var expr = orExpr(stream, a); + if (null == expr) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected expression after ['); + if (null == stream.trypop(']')) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected ] after expression.'); + return a.node('Predicate', lhs, expr); + } + /** [14] Expr::= OrExpr + */ + /** [15] PrimaryExpr::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall + * e.g. $x, (3+4), "hi", 32, f(x) + */ + function primaryExpr(stream, a) { + var x = stream.trypopliteral(); + if (null == x) + x = stream.trypopnumber(); + if (null != x) { + return x; + } + var varRef = stream.trypopvarref(); + if (null != varRef) return a.node('VariableReference', varRef); + var funCall = functionCall(stream, a); + if (null != funCall) { + return funCall; + } + if (stream.trypop('(')) { + var e = orExpr(stream, a); + if (null == e) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected expression after (.'); + if (null == stream.trypop(')')) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected ) after expression.'); + return e; + } + return null; + } + /** [16] FunctionCall::= FunctionName '(' ( Argument ( ',' Argument )* )? ')' + * [17] Argument::= Expr + */ + function functionCall(stream, a) { + var name = stream.trypopfuncname(stream, a); + if (null == name) return null; + if (null == stream.trypop('(')) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected ( ) after function name.'); + var params = []; + var first = true; + while (null == stream.trypop(')')) { + if (!first && null == stream.trypop(',')) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected , between arguments of the function.'); + first = false; + var param = orExpr(stream, a); + if (param == null) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected expression as argument of function.'); + params.push(param); + } + return a.node('FunctionCall', name, params); + } + + /** [18] UnionExpr::= PathExpr | UnionExpr '|' PathExpr + */ + function unionExpr(stream, a) { return binaryL(pathExpr, stream, a, '|'); } + /** [19] PathExpr ::= LocationPath + * | FilterExpr + * | FilterExpr '/' RelativeLocationPath + * | FilterExpr '//' RelativeLocationPath + * Unlike most other nodes, this one always generates a node because + * at this point all reverse nodesets must turn into a forward nodeset + */ + function pathExpr(stream, a) { + // We have to do FilterExpr before LocationPath because otherwise + // LocationPath will eat up the name from a function call. + var filter = filterExpr(stream, a); + if (null == filter) { + var loc = locationPath(stream, a); + if (null == loc) { + throw new Error + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': The expression shouldn\'t be empty...'); + } + return a.node('PathExpr', loc); + } + var rel = relativeLocationPath(filter, stream, a, false); + if (filter === rel) return rel; + else return a.node('PathExpr', rel); + } + /** [20] FilterExpr::= PrimaryExpr | FilterExpr Predicate + * aka. FilterExpr ::= PrimaryExpr Predicate* + */ + function filterExpr(stream, a) { + var primary = primaryExpr(stream, a); + if (primary == null) return null; + var pred, lhs = primary; + while (null != (pred = predicate(lhs, stream, a))) { + lhs = pred; + } + return lhs; + } + + /** [21] OrExpr::= AndExpr | OrExpr 'or' AndExpr + */ + function orExpr(stream, a) { + var orig = (stream.peeked || '') + stream.str + var r = binaryL(andExpr, stream, a, 'or'); + var now = (stream.peeked || '') + stream.str; + return r; + } + /** [22] AndExpr::= EqualityExpr | AndExpr 'and' EqualityExpr + */ + function andExpr(stream, a) { return binaryL(equalityExpr, stream, a, 'and'); } + /** [23] EqualityExpr::= RelationalExpr | EqualityExpr '=' RelationalExpr + * | EqualityExpr '!=' RelationalExpr + */ + function equalityExpr(stream, a) { return binaryL(relationalExpr, stream, a, ['=','!=']); } + /** [24] RelationalExpr::= AdditiveExpr | RelationalExpr '<' AdditiveExpr + * | RelationalExpr '>' AdditiveExpr + * | RelationalExpr '<=' AdditiveExpr + * | RelationalExpr '>=' AdditiveExpr + */ + function relationalExpr(stream, a) { return binaryL(additiveExpr, stream, a, ['<','>','<=','>=']); } + /** [25] AdditiveExpr::= MultiplicativeExpr + * | AdditiveExpr '+' MultiplicativeExpr + * | AdditiveExpr '-' MultiplicativeExpr + */ + function additiveExpr(stream, a) { return binaryL(multiplicativeExpr, stream, a, ['+','-']); } + /** [26] MultiplicativeExpr::= UnaryExpr + * | MultiplicativeExpr MultiplyOperator UnaryExpr + * | MultiplicativeExpr 'div' UnaryExpr + * | MultiplicativeExpr 'mod' UnaryExpr + */ + function multiplicativeExpr(stream, a) { return binaryL(unaryExpr, stream, a, ['*','div','mod']); } + /** [27] UnaryExpr::= UnionExpr | '-' UnaryExpr + */ + function unaryExpr(stream, a) { + if (stream.trypop('-')) { + var e = unaryExpr(stream, a); + if (null == e) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Expected unary expression after -'); + return a.node('UnaryMinus', e); + } + else return unionExpr(stream, a); + } + var astFactory = { + node: function() {return Array.prototype.slice.call(arguments);} + }; + + + /*************************************************************************** + * Optimizations (TODO) * + ***************************************************************************/ + /** + * Some things I've been considering: + * 1) a//b becomes a/descendant::b if there's no predicate that uses + * position() or last() + * 2) axis[pred]: when pred doesn't use position, evaluate it just once per + * node in the node-set rather than once per (node, position, last). + * For more optimizations, look up Gecko's optimizer: + * http://mxr.mozilla.org/mozilla-central/source/content/xslt/src/xpath/txXPathOptimizer.cpp + */ + // TODO + function optimize(ast) { + } + + /*************************************************************************** + * Evaluation: axes * + ***************************************************************************/ + + /** + * Data types: For string, number, boolean, we just use Javascript types. + * Node-sets have the form + * {nodes: [node, ...]} + * or {nodes: [node, ...], pos: [[1], [2], ...], lasts: [[1], [2], ...]} + * + * Most of the time, only the node is used and the position information is + * discarded. But if you use a predicate, we need to try every value of + * position and last in case the predicate calls position() or last(). + */ + + /** + * The NodeMultiSet is a helper class to help generate + * {nodes:[], pos:[], lasts:[]} structures. It is useful for the + * descendant, descendant-or-self, following-sibling, and + * preceding-sibling axes for which we can use a stack to organize things. + */ + function NodeMultiSet(isReverseAxis) { + this.nodes = []; + this.pos = []; + this.lasts = []; + this.nextPos = []; + this.seriesIndexes = []; // index within nodes that each series begins. + this.isReverseAxis = isReverseAxis; + this._pushToNodes = isReverseAxis ? Array.prototype.unshift : Array.prototype.push; + } + NodeMultiSet.prototype = { + pushSeries: function pushSeries() { + this.nextPos.push(1); + this.seriesIndexes.push(this.nodes.length); + }, + popSeries: function popSeries() { + console.assert(0 < this.nextPos.length, this.nextPos); + var last = this.nextPos.pop() - 1, + indexInPos = this.nextPos.length, + seriesBeginIndex = this.seriesIndexes.pop(), + seriesEndIndex = this.nodes.length; + for (var i = seriesBeginIndex; i < seriesEndIndex; ++i) { + console.assert(indexInPos < this.lasts[i].length); + console.assert(undefined === this.lasts[i][indexInPos]); + this.lasts[i][indexInPos] = last; + } + }, + finalize: function() { + if (null == this.nextPos) return this; + console.assert(0 === this.nextPos.length); + for (var i = 0; i < this.lasts.length; ++i) { + for (var j = 0; j < this.lasts[i].length; ++j) { + console.assert(null != this.lasts[i][j], i + ',' + j + ':' + JSON.stringify(this.lasts)); + } + } + this.pushSeries = this.popSeries = this.addNode = function() { + throw new Error('Already finalized.'); + }; + return this; + }, + addNode: function addNode(node) { + console.assert(node); + this._pushToNodes.call(this.nodes, node) + this._pushToNodes.call(this.pos, this.nextPos.slice()); + this._pushToNodes.call(this.lasts, new Array(this.nextPos.length)); + for (var i = 0; i < this.nextPos.length; ++i) this.nextPos[i]++; + }, + simplify: function() { + this.finalize(); + return {nodes:this.nodes, pos:this.pos, lasts:this.lasts}; + } + }; + function eachContext(nodeMultiSet) { + var r = []; + for (var i = 0; i < nodeMultiSet.nodes.length; i++) { + var node = nodeMultiSet.nodes[i]; + if (!nodeMultiSet.pos) { + r.push({nodes:[node], pos: [[i + 1]], lasts: [[nodeMultiSet.nodes.length]]}); + } else { + for (var j = 0; j < nodeMultiSet.pos[i].length; ++j) { + r.push({nodes:[node], pos: [[nodeMultiSet.pos[i][j]]], lasts: [[nodeMultiSet.lasts[i][j]]]}); + } + } + } + return r; + } + /** Matcher used in the axes. + */ + function NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase) { + this.nodeTypeNum = nodeTypeNum; + this.nodeName = nodeName; + this.shouldLowerCase = shouldLowerCase; + this.nodeNameTest = + null == nodeName ? this._alwaysTrue : + shouldLowerCase ? this._nodeNameLowerCaseEquals : + this._nodeNameEquals; + } + NodeMatcher.prototype = { + matches: function matches(node) { + return (0 === this.nodeTypeNum || node.nodeType === this.nodeTypeNum) && + this.nodeNameTest(node.nodeName); + }, + _alwaysTrue: function(name) {return true;}, + _nodeNameEquals: function _nodeNameEquals(name) { + return this.nodeName === name; + }, + _nodeNameLowerCaseEquals: function _nodeNameLowerCaseEquals(name) { + return this.nodeName === name.toLowerCase(); + } + }; + + function followingSiblingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, shift, peek, followingNode, andSelf, isReverseAxis) { + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + var nodeMultiSet = new NodeMultiSet(isReverseAxis); + while (0 < nodeList.length) { // can be if for following, preceding + var node = shift.call(nodeList); + console.assert(node != null); + node = followingNode(node); + nodeMultiSet.pushSeries(); + var numPushed = 1; + while (null != node) { + if (! andSelf && matcher.matches(node)) + nodeMultiSet.addNode(node); + if (node === peek.call(nodeList)) { + shift.call(nodeList); + nodeMultiSet.pushSeries(); + numPushed++; + } + if (andSelf && matcher.matches(node)) + nodeMultiSet.addNode(node); + node = followingNode(node); + } + while (0 < numPushed--) + nodeMultiSet.popSeries(); + } + return nodeMultiSet; + } + + /** Returns the next non-descendant node in document order. + * This is the first node in following::node(), if node is the context. + */ + function followingNonDescendantNode(node) { + if (node.ownerElement) { + if (node.ownerElement.firstChild) + return node.ownerElement.firstChild; + node = node.ownerElement; + } + do { + if (node.nextSibling) return node.nextSibling; + } while (node = node.parentNode); + return null; + } + + /** Returns the next node in a document-order depth-first search. + * See the definition of document order[1]: + * 1) element + * 2) namespace nodes + * 3) attributes + * 4) children + * [1]: http://www.w3.org/TR/xpath/#dt-document-order + */ + function followingNode(node) { + if (node.ownerElement) // attributes: following node of element. + node = node.ownerElement; + if (null != node.firstChild) + return node.firstChild; + do { + if (null != node.nextSibling) { + return node.nextSibling; + } + node = node.parentNode; + } while (node); + return null; + } + /** Returns the previous node in document order (excluding attributes + * and namespace nodes). + */ + function precedingNode(node) { + if (node.ownerElement) + return node.ownerElement; + if (null != node.previousSibling) { + node = node.previousSibling; + while (null != node.lastChild) { + node = node.lastChild; + } + return node; + } + if (null != node.parentNode) { + return node.parentNode; + } + return null; + } + /** This axis is inefficient if there are many nodes in the nodeList. + * But I think it's a pretty useless axis so it's ok. */ + function followingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + var nodeMultiSet = new NodeMultiSet(false); + var cursor = nodeList[0]; + var unorderedFollowingStarts = []; + for (var i = 0; i < nodeList.length; i++) { + var node = nodeList[i]; + var start = followingNonDescendantNode(node); + if (start) + unorderedFollowingStarts.push(start); + } + if (0 === unorderedFollowingStarts.length) + return {nodes:[]}; + var pos = [], nextPos = []; + var started = 0; + while (cursor = followingNode(cursor)) { + for (var i = unorderedFollowingStarts.length - 1; i >= 0; i--){ + if (cursor === unorderedFollowingStarts[i]) { + nodeMultiSet.pushSeries(); + unorderedFollowingStarts.splice(i,i+1); + started++; + } + } + if (started && matcher.matches(cursor)) { + nodeMultiSet.addNode(cursor); + } + } + console.assert(0 === unorderedFollowingStarts.length); + for (var i = 0; i < started; i++) + nodeMultiSet.popSeries(); + return nodeMultiSet.finalize(); + } + function precedingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + var cursor = nodeList.pop(); + if (null == cursor) return {nodes:{}}; + var r = {nodes:[], pos:[], lasts:[]}; + var nextParents = [cursor.parentNode || cursor.ownerElement], nextPos = [1]; + while (cursor = precedingNode(cursor)) { + if (cursor === nodeList[nodeList.length - 1]) { + nextParents.push(nodeList.pop()); + nextPos.push(1); + } + var matches = matcher.matches(cursor); + var pos, someoneUsed = false; + if (matches) + pos = nextPos.slice(); + + for (var i = 0; i < nextParents.length; ++i) { + if (cursor === nextParents[i]) { + nextParents[i] = cursor.parentNode || cursor.ownerElement; + if (matches) { + pos[i] = null; + } + } else { + if (matches) { + pos[i] = nextPos[i]++; + someoneUsed = true; + } + } + } + if (someoneUsed) { + r.nodes.unshift(cursor); + r.pos.unshift(pos); + } + } + for (var i = 0; i < r.pos.length; ++i) { + var lasts = []; + r.lasts.push(lasts); + for (var j = r.pos[i].length - 1; j >= 0; j--) { + if (null == r.pos[i][j]) { + r.pos[i].splice(j, j+1); + } else { + lasts.unshift(nextPos[j] - 1); + } + } + } + return r; + } + + /** node-set, axis -> node-set */ + function descendantDfs(nodeMultiSet, node, remaining, matcher, andSelf, attrIndices, attrNodes) { + while (0 < remaining.length && null != remaining[0].ownerElement) { + var attr = remaining.shift(); + if (andSelf && matcher.matches(attr)) { + attrNodes.push(attr); + attrIndices.push(nodeMultiSet.nodes.length); + } + } + if (null != node && !andSelf) { + if (matcher.matches(node)) + nodeMultiSet.addNode(node); + } + var pushed = false; + if (null == node) { + if (0 === remaining.length) return; + node = remaining.shift(); + nodeMultiSet.pushSeries(); + pushed = true; + } else if (0 < remaining.length && node === remaining[0]) { + nodeMultiSet.pushSeries(); + pushed = true; + remaining.shift(); + } + if (andSelf) { + if (matcher.matches(node)) + nodeMultiSet.addNode(node); + } + // TODO: use optimization. Also try element.getElementsByTagName + // var nodeList = 1 === nodeTypeNum && null != node.children ? node.children : node.childNodes; + var nodeList = node.childNodes; + for (var j = 0; j < nodeList.length; ++j) { + var child = nodeList[j]; + descendantDfs(nodeMultiSet, child, remaining, matcher, andSelf, attrIndices, attrNodes); + } + if (pushed) { + nodeMultiSet.popSeries(); + } + } + function descenantHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, andSelf) { + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + var nodeMultiSet = new NodeMultiSet(false); + var attrIndices = [], attrNodes = []; + while (0 < nodeList.length) { + // var node = nodeList.shift(); + descendantDfs(nodeMultiSet, null, nodeList, matcher, andSelf, attrIndices, attrNodes); + } + nodeMultiSet.finalize(); + for (var i = attrNodes.length-1; i >= 0; --i) { + nodeMultiSet.nodes.splice(attrIndices[i], attrIndices[i], attrNodes[i]); + nodeMultiSet.pos.splice(attrIndices[i], attrIndices[i], [1]); + nodeMultiSet.lasts.splice(attrIndices[i], attrIndices[i], [1]); + } + return nodeMultiSet; + } + /** + */ + function ancestorHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, andSelf) { + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + var ancestors = []; // array of non-empty arrays of matching ancestors + for (var i = 0; i < nodeList.length; ++i) { + var node = nodeList[i]; + var isFirst = true; + var a = []; + while (null != node) { + if (!isFirst || andSelf) { + if (matcher.matches(node)) + a.push(node); + } + isFirst = false; + node = node.parentNode || node.ownerElement; + } + if (0 < a.length) + ancestors.push(a); + } + var lasts = []; + for (var i = 0; i < ancestors.length; ++i) lasts.push(ancestors[i].length); + var nodeMultiSet = new NodeMultiSet(true); + var newCtx = {nodes:[], pos:[], lasts:[]}; + while (0 < ancestors.length) { + var pos = [ancestors[0].length]; + var last = [lasts[0]]; + var node = ancestors[0].pop(); + for (var i = ancestors.length - 1; i > 0; --i) { + if (node === ancestors[i][ancestors[i].length - 1]) { + pos.push(ancestors[i].length); + last.push(lasts[i]); + ancestors[i].pop(); + if (0 === ancestors[i].length) { + ancestors.splice(i, i+1); + lasts.splice(i, i+1); + } + } + } + if (0 === ancestors[0].length) { + ancestors.shift(); + lasts.shift(); + } + newCtx.nodes.push(node); + newCtx.pos.push(pos); + newCtx.lasts.push(last); + } + return newCtx; + } + /** Helper function for sortDocumentOrder. Returns a list of indices, from the + * node to the root, of positions within parent. + * For convenience, the node is the first element of the array. + */ + function addressVector(node) { + var r = [node]; + if (null != node.ownerElement) { + node = node.ownerElement; + r.push(-1); + } + while (null != node) { + var i = 0; + while (null != node.previousSibling) { + node = node.previousSibling; + i++; + } + r.push(i); + node = node.parentNode + } + return r; + } + function addressComparator(a, b) { + var minlen = Math.min(a.length - 1, b.length - 1), // not including [0]=node + alen = a.length, + blen = b.length; + if (a[0] === b[0]) return 0; + var c; + for (var i = 0; i < minlen; ++i) { + c = a[alen - i - 1] - b[blen - i - 1]; + if (0 !== c) + break; + } + if (null == c || 0 === c) { + // All equal until one of the nodes. The longer one is the descendant. + c = alen - blen; + } + if (0 === c) + c = a.nodeName - b.nodeName; + if (0 === c) + c = 1; + return c; + } + var sortUniqDocumentOrder = xpath.sortUniqDocumentOrder = function(nodes) { + var a = []; + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + var v = addressVector(node); + a.push(v); + } + a.sort(addressComparator); + var b = []; + for (var i = 0; i < a.length; i++) { + if (0 < i && a[i][0] === a[i - 1][0]) + continue; + b.push(a[i][0]); + } + return b; + } + /** Sort node multiset. Does not do any de-duping. */ + function sortNodeMultiSet(nodeMultiSet) { + var a = []; + for (var i = 0; i < nodeMultiSet.nodes.length; i++) { + var v = addressVector(nodeMultiSet.nodes[i]); + a.push({v:v, n:nodeMultiSet.nodes[i], + p:nodeMultiSet.pos[i], l:nodeMultiSet.lasts[i]}); + } + a.sort(compare); + var r = {nodes:[], pos:[], lasts:[]}; + for (var i = 0; i < a.length; ++i) { + r.nodes.push(a[i].n); + r.pos.push(a[i].p); + r.lasts.push(a[i].l); + } + function compare(x, y) { + return addressComparator(x.v, y.v); + } + return r; + } + /** Returns an array containing all the ancestors down to a node. + * The array starts with document. + */ + function nodeAndAncestors(node) { + var ancestors = [node]; + var p = node; + while (p = p.parentNode || p.ownerElement) { + ancestors.unshift(p); + } + return ancestors; + } + function compareSiblings(a, b) { + if (a === b) return 0; + var c = a; + while (c = c.previousSibling) { + if (c === b) + return 1; // b < a + } + c = b; + while (c = c.previousSibling) { + if (c === a) + return -1; // a < b + } + throw new Error('a and b are not siblings: ' + xpath.stringifyObject(a) + ' vs ' + xpath.stringifyObject(b)); + } + /** The merge in merge-sort.*/ + function mergeNodeLists(x, y) { + var a, b, aanc, banc, r = []; + if ('object' !== typeof x) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Invalid LHS for | operator ' + + '(expected node-set): ' + x); + if ('object' !== typeof y) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Invalid LHS for | operator ' + + '(expected node-set): ' + y); + while (true) { + if (null == a) { + a = x.shift(); + if (null != a) + aanc = addressVector(a); + } + if (null == b) { + b = y.shift(); + if (null != b) + banc = addressVector(b); + } + if (null == a || null == b) break; + var c = addressComparator(aanc, banc); + if (c < 0) { + r.push(a); + a = null; + aanc = null; + } else if (c > 0) { + r.push(b); + b = null; + banc = null; + } else if (a.nodeName < b.nodeName) { // attributes + r.push(a); + a = null; + aanc = null; + } else if (a.nodeName > b.nodeName) { // attributes + r.push(b); + b = null; + banc = null; + } else if (a !== b) { + // choose b arbitrarily + r.push(b); + b = null; + banc = null; + } else { + console.assert(a === b, c); + // just skip b without pushing it. + b = null; + banc = null; + } + } + while (a) { + r.push(a); + a = x.shift(); + } + while (b) { + r.push(b); + b = y.shift(); + } + return r; + } + function comparisonHelper(test, x, y, isNumericComparison) { + var coersion; + if (isNumericComparison) + coersion = fn.number; + else coersion = + 'boolean' === typeof x || 'boolean' === typeof y ? fn['boolean'] : + 'number' === typeof x || 'number' === typeof y ? fn.number : + fn.string; + if ('object' === typeof x && 'object' === typeof y) { + var aMap = {}; + for (var i = 0; i < x.nodes.length; ++i) { + var xi = coersion({nodes:[x.nodes[i]]}); + for (var j = 0; j < y.nodes.length; ++j) { + var yj = coersion({nodes:[y.nodes[j]]}); + if (test(xi, yj)) return true; + } + } + return false; + } else if ('object' === typeof x && x.nodes && x.nodes.length) { + for (var i = 0; i < x.nodes.length; ++i) { + var xi = coersion({nodes:[x.nodes[i]]}), yc = coersion(y); + if (test(xi, yc)) + return true; + } + return false; + } else if ('object' === typeof y && x.nodes && x.nodes.length) { + for (var i = 0; i < x.nodes.length; ++i) { + var yi = coersion({nodes:[y.nodes[i]]}), xc = coersion(x); + if (test(xc, yi)) + return true; + } + return false; + } else { + var xc = coersion(x), yc = coersion(y); + return test(xc, yc); + } + } + var axes = xpath.axes = { + 'ancestor': + function ancestor(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + return ancestorHelper( + nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, false); + }, + 'ancestor-or-self': + function ancestorOrSelf(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + return ancestorHelper( + nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, true); + }, + 'attribute': + function attribute(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + // TODO: figure out whether positions should be undefined here. + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + var nodeMultiSet = new NodeMultiSet(false); + if (null != nodeName) { + // TODO: with namespace + for (var i = 0; i < nodeList.length; ++i) { + var node = nodeList[i]; + if (null == node.getAttributeNode) + continue; // only Element has .getAttributeNode + var attr = node.getAttributeNode(nodeName); + if (null != attr && matcher.matches(attr)) { + nodeMultiSet.pushSeries(); + nodeMultiSet.addNode(attr); + nodeMultiSet.popSeries(); + } + } + } else { + for (var i = 0; i < nodeList.length; ++i) { + var node = nodeList[i]; + if (null != node.attributes) { + nodeMultiSet.pushSeries(); + for (var j = 0; j < node.attributes.length; j++) { // all nodes have .attributes + var attr = node.attributes[j]; + if (matcher.matches(attr)) // TODO: I think this check is unnecessary + nodeMultiSet.addNode(attr); + } + nodeMultiSet.popSeries(); + } + } + } + return nodeMultiSet.finalize(); + }, + 'child': + function child(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + var nodeMultiSet = new NodeMultiSet(false); + for (var i = 0; i < nodeList.length; ++i) { + var n = nodeList[i]; + if (n.ownerElement) // skip attribute nodes' text child. + continue; + if (n.childNodes) { + nodeMultiSet.pushSeries(); + var childList = 1 === nodeTypeNum && null != n.children ? + n.children : n.childNodes; + for (var j = 0; j < childList.length; ++j) { + var child = childList[j]; + if (matcher.matches(child)) { + nodeMultiSet.addNode(child); + } + // don't have to do de-duping because children have parent, + // which are current context. + } + nodeMultiSet.popSeries(); + } + } + nodeMultiSet.finalize(); + return sortNodeMultiSet(nodeMultiSet); + }, + 'descendant': + function descenant(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + return descenantHelper( + nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, false); + }, + 'descendant-or-self': + function descenantOrSelf(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + return descenantHelper( + nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, true); + }, + 'following': + function following(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + return followingHelper(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase); + }, + 'following-sibling': + function followingSibling(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + return followingSiblingHelper( + nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, + Array.prototype.shift, function() {return this[0];}, + function(node) {return node.nextSibling;}); + }, + 'namespace': + function namespace(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + // TODO + }, + 'parent': + function parent(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + var nodes = [], pos = []; + for (var i = 0; i < nodeList.length; ++i) { + var parent = nodeList[i].parentNode || nodeList[i].ownerElement; + if (null == parent) + continue; + if (!matcher.matches(parent)) + continue; + if (nodes.length > 0 && parent === nodes[nodes.length-1]) + continue; + nodes.push(parent); + pos.push([1]); + } + return {nodes:nodes, pos:pos, lasts:pos}; + }, + 'preceding': + function preceding(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + return precedingHelper( + nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase); + }, + 'preceding-sibling': + function precedingSibling(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + return followingSiblingHelper( + nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase, + Array.prototype.pop, function() {return this[this.length-1];}, + function(node) {return node.previousSibling}, + false, true); + }, + 'self': + function self(nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase) { + var nodes = [], pos = []; + var matcher = new NodeMatcher(nodeTypeNum, nodeName, shouldLowerCase); + for (var i = 0; i < nodeList.length; ++i) { + if (matcher.matches(nodeList[i])) { + nodes.push(nodeList[i]); + pos.push([1]); + } + } + return {nodes: nodes, pos: pos, lasts: pos} + } + }; + + /*************************************************************************** + * Evaluation: functions * + ***************************************************************************/ + var fn = { + 'number': function number(optObject) { + if ('number' === typeof optObject) + return optObject; + if ('string' === typeof optObject) + return parseFloat(optObject); // note: parseFloat(' ') -> NaN, unlike +' ' -> 0. + if ('boolean' === typeof optObject) + return +optObject; + return fn.number(fn.string.call(this, optObject)); // for node-sets + }, + 'string': function string(optObject) { + if (null == optObject) + return fn.string(this); + if ('string' === typeof optObject || 'boolean' === typeof optObject || + 'number' === typeof optObject) + return '' + optObject; + if (0 == optObject.nodes.length) return ''; + if (null != optObject.nodes[0].textContent) + return optObject.nodes[0].textContent; + return optObject.nodes[0].nodeValue; + }, + 'boolean': function booleanVal(x) { + return 'object' === typeof x ? x.nodes.length > 0 : !!x; + }, + 'last': function last() { + console.assert(Array.isArray(this.pos)); + console.assert(Array.isArray(this.lasts)); + console.assert(1 === this.pos.length); + console.assert(1 === this.lasts.length); + console.assert(1 === this.lasts[0].length); + return this.lasts[0][0]; + }, + 'position': function position() { + console.assert(Array.isArray(this.pos)); + console.assert(Array.isArray(this.lasts)); + console.assert(1 === this.pos.length); + console.assert(1 === this.lasts.length); + console.assert(1 === this.pos[0].length); + return this.pos[0][0]; + }, + 'count': function count(nodeSet) { + if ('object' !== typeof nodeSet) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Position ' + stream.position() + + ': Function count(node-set) ' + + 'got wrong argument type: ' + nodeSet); + return nodeSet.nodes.length; + }, + 'id': function id(object) { + var r = {nodes: []}; + var doc = this.nodes[0].ownerDocument || this.nodes[0]; + console.assert(doc); + var ids; + if ('object' === typeof object) { + // for node-sets, map id over each node value. + ids = []; + for (var i = 0; i < object.nodes.length; ++i) { + var idNode = object.nodes[i]; + var idsString = fn.string({nodes:[idNode]}); + var a = idsString.split(/[ \t\r\n]+/g); + Array.prototype.push.apply(ids, a); + } + } else { + var idsString = fn.string(object); + var a = idsString.split(/[ \t\r\n]+/g); + ids = a; + } + for (var i = 0; i < ids.length; ++i) { + var id = ids[i]; + if (0 === id.length) + continue; + var node = doc.getElementById(id); + if (null != node) + r.nodes.push(node); + } + r.nodes = sortUniqDocumentOrder(r.nodes); + return r; + }, + 'local-name': function(nodeSet) { + if (null == nodeSet) + return fn.name(this); + if (null == nodeSet.nodes) { + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'argument to name() must be a node-set. got ' + nodeSet); + } + // TODO: namespaced version + return nodeSet.nodes[0].nodeName.toLowerCase(); // TODO: no toLowerCase for xml + }, + 'namespace-uri': function(nodeSet) { + // TODO + throw new Error('not implemented yet'); + }, + 'name': function(nodeSet) { + if (null == nodeSet) + return fn.name(this); + if (null == nodeSet.nodes) { + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'argument to name() must be a node-set. got ' + nodeSet); + } + return nodeSet.nodes[0].nodeName.toLowerCase(); // TODO: no toLowerCase for xml + }, + 'concat': function concat(x) { + var l = []; + for (var i = 0; i < arguments.length; ++i) { + l.push(fn.string(arguments[i])); + } + return l.join(''); + }, + 'starts-with': function startsWith(a, b) { + var as = fn.string(a), bs = fn.string(b); + return as.substr(0, bs.length) === bs; + }, + 'contains': function contains(a, b) { + var as = fn.string(a), bs = fn.string(b); + var i = as.indexOf(bs); + if (-1 === i) return false; + return true; + }, + 'substring-before': function substringBefore(a, b) { + var as = fn.string(a), bs = fn.string(b); + var i = as.indexOf(bs); + if (-1 === i) return ''; + return as.substr(0, i); + }, + 'substring-after': function substringBefore(a, b) { + var as = fn.string(a), bs = fn.string(b); + var i = as.indexOf(bs); + if (-1 === i) return ''; + return as.substr(i + bs.length); + }, + 'substring': function substring(string, start, optEnd) { + if (null == string || null == start) { + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Must be at least 2 arguments to string()'); + } + var sString = fn.string(string), + iStart = fn.round(start), + iEnd = optEnd == null ? null : fn.round(optEnd); + // Note that xpath string positions user 1-based index + if (iEnd == null) + return sString.substr(iStart - 1); + else + return sString.substr(iStart - 1, iEnd); + }, + 'string-length': function stringLength(optString) { + return fn.string.call(this, optString).length; + }, + 'normalize-space': function normalizeSpace(optString) { + var s = fn.string.call(this, optString); + return s.replace(/[ \t\r\n]+/g, ' ').replace(/^ | $/g, ''); + }, + 'translate': function translate(string, from, to) { + var sString = fn.string.call(this, string), + sFrom = fn.string(from), + sTo = fn.string(to); + var eachCharRe = []; + var map = {}; + for (var i = 0; i < sFrom.length; ++i) { + var c = sFrom.charAt(i); + map[c] = sTo.charAt(i); // returns '' if beyond length of sTo. + // copied from goog.string.regExpEscape in the Closure library. + eachCharRe.push( + c.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). + replace(/\x08/g, '\\x08')); + } + var re = new RegExp(eachCharRe.join('|'), 'g'); + return sString.replace(re, function(c) {return map[c];}); + }, + /// Boolean functions + 'not': function not(x) { + var bx = fn['boolean'](x); + return !bx; + }, + 'true': function trueVal() { return true; }, + 'false': function falseVal() { return false; }, + // TODO + 'lang': function lang(string) { throw new Error('Not implemented');}, + 'sum': function sum(optNodeSet) { + if (null == optNodeSet) return fn.sum(this); + // for node-sets, map id over each node value. + var sum = 0; + for (var i = 0; i < optNodeSet.nodes.length; ++i) { + var node = optNodeSet.nodes[i]; + var x = fn.number({nodes:[node]}); + sum += x; + } + return sum; + }, + 'floor': function floor(number) { + return Math.floor(fn.number(number)); + }, + 'ceiling': function ceiling(number) { + return Math.ceil(fn.number(number)); + }, + 'round': function round(number) { + return Math.round(fn.number(number)); + } + }; + /*************************************************************************** + * Evaluation: operators * + ***************************************************************************/ + var more = { + UnaryMinus: function(x) { return -fn.number(x); }, + '+': function(x, y) { return fn.number(x) + fn.number(y); }, + '-': function(x, y) { return fn.number(x) - fn.number(y); }, + '*': function(x, y) { return fn.number(x) * fn.number(y); }, + 'div': function(x, y) { return fn.number(x) / fn.number(y); }, + 'mod': function(x, y) { return fn.number(x) % fn.number(y); }, + '<': function(x, y) { + return comparisonHelper(function(x, y) { return fn.number(x) < fn.number(y);}, x, y, true); + }, + '<=': function(x, y) { + return comparisonHelper(function(x, y) { return fn.number(x) <= fn.number(y);}, x, y, true); + }, + '>': function(x, y) { + return comparisonHelper(function(x, y) { return fn.number(x) > fn.number(y);}, x, y, true); + }, + '>=': function(x, y) { + return comparisonHelper(function(x, y) { return fn.number(x) >= fn.number(y);}, x, y, true); + }, + 'and': function(x, y) { return fn['boolean'](x) && fn['boolean'](y); }, + 'or': function(x, y) { return fn['boolean'](x) || fn['boolean'](y); }, + '|': function(x, y) { return {nodes: mergeNodeLists(x.nodes, y.nodes)}; }, + '=': function(x, y) { + // optimization for two node-sets case: avoid n^2 comparisons. + if ('object' === typeof x && 'object' === typeof y) { + var aMap = {}; + for (var i = 0; i < x.nodes.length; ++i) { + var s = fn.string({nodes:[x.nodes[i]]}); + aMap[s] = true; + } + for (var i = 0; i < y.nodes.length; ++i) { + var s = fn.string({nodes:[y.nodes[i]]}); + if (aMap[s]) return true; + } + return false; + } else { + return comparisonHelper(function(x, y) {return x === y;}, x, y); + } + }, + '!=': function(x, y) { + // optimization for two node-sets case: avoid n^2 comparisons. + if ('object' === typeof x && 'object' === typeof y) { + if (0 === x.nodes.length || 0 === y.nodes.length) return false; + var aMap = {}; + for (var i = 0; i < x.nodes.length; ++i) { + var s = fn.string({nodes:[x.nodes[i]]}); + aMap[s] = true; + } + for (var i = 0; i < y.nodes.length; ++i) { + var s = fn.string({nodes:[y.nodes[i]]}); + if (!aMap[s]) return true; + } + return false; + } else { + return comparisonHelper(function(x, y) {return x !== y;}, x, y); + } + } + }; + var nodeTypes = xpath.nodeTypes = { + 'node': 0, + 'attribute': 2, + 'comment': 8, // this.doc.COMMENT_NODE, + 'text': 3, // this.doc.TEXT_NODE, + 'processing-instruction': 7, // this.doc.PROCESSING_INSTRUCTION_NODE, + 'element': 1 //this.doc.ELEMENT_NODE + }; + /** For debugging and unit tests: returnjs a stringified version of the + * argument. */ + var stringifyObject = xpath.stringifyObject = function stringifyObject(ctx) { + var seenKey = 'seen' + Math.floor(Math.random()*1000000000); + return JSON.stringify(helper(ctx)); + + function helper(ctx) { + if (Array.isArray(ctx)) { + return ctx.map(function(x) {return helper(x);}); + } + if ('object' !== typeof ctx) return ctx; + if (null == ctx) return ctx; + // if (ctx.toString) return ctx.toString(); + if (null != ctx.outerHTML) return ctx.outerHTML; + if (null != ctx.nodeValue) return ctx.nodeName + '=' + ctx.nodeValue; + if (ctx[seenKey]) return '[circular]'; + ctx[seenKey] = true; + var nicer = {}; + for (var key in ctx) { + if (seenKey === key) + continue; + try { + nicer[key] = helper(ctx[key]); + } catch (e) { + nicer[key] = '[exception: ' + e.message + ']'; + } + } + delete ctx[seenKey]; + return nicer; + } + } + var Evaluator = xpath.Evaluator = function Evaluator(doc) { + this.doc = doc; + } + Evaluator.prototype = { + val: function val(ast, ctx) { + console.assert(ctx.nodes); + + if ('number' === typeof ast || 'string' === typeof ast) return ast; + if (more[ast[0]]) { + var evaluatedParams = []; + for (var i = 1; i < ast.length; ++i) { + evaluatedParams.push(this.val(ast[i], ctx)); + } + var r = more[ast[0]].apply(ctx, evaluatedParams); + return r; + } + switch (ast[0]) { + case 'Root': return {nodes: [this.doc]}; + case 'FunctionCall': + var functionName = ast[1], functionParams = ast[2]; + if (null == fn[functionName]) + throw new XPathException(XPathException.INVALID_EXPRESSION_ERR, + 'Unknown function: ' + functionName); + var evaluatedParams = []; + for (var i = 0; i < functionParams.length; ++i) { + evaluatedParams.push(this.val(functionParams[i], ctx)); + } + var r = fn[functionName].apply(ctx, evaluatedParams); + return r; + case 'Predicate': + var lhs = this.val(ast[1], ctx); + var ret = {nodes: []}; + var contexts = eachContext(lhs); + for (var i = 0; i < contexts.length; ++i) { + var singleNodeSet = contexts[i]; + var rhs = this.val(ast[2], singleNodeSet); + var success; + if ('number' === typeof rhs) { + success = rhs === singleNodeSet.pos[0][0]; + } else { + success = fn['boolean'](rhs); + } + if (success) { + var node = singleNodeSet.nodes[0]; + ret.nodes.push(node); + // skip over all the rest of the same node. + while (i+1 < contexts.length && node === contexts[i+1].nodes[0]) { + i++; + } + } + } + return ret; + case 'PathExpr': + // turn the path into an expressoin; i.e., remove the position + // information of the last axis. + var x = this.val(ast[1], ctx); + // Make the nodeset a forward-direction-only one. + if (x.finalize) { // it is a NodeMultiSet + for (var i = 0; i < x.nodes.length; ++i) { + console.assert(null != x.nodes[i].nodeType); + } + return {nodes: x.nodes}; + } else { + return x; + } + case '/': + // TODO: don't generate '/' nodes, just Axis nodes. + var lhs = this.val(ast[1], ctx); + console.assert(null != lhs); + var r = this.val(ast[2], lhs); + console.assert(null != r); + return r; + case 'Axis': + // All the axis tests from Step. We only get AxisSpecifier NodeTest, + // not the predicate (which is applied later) + var axis = ast[1], + nodeType = ast[2], + nodeTypeNum = nodeTypes[nodeType], + shouldLowerCase = true, // TODO: give option + nodeName = ast[3] && shouldLowerCase ? ast[3].toLowerCase() : ast[3]; + nodeName = nodeName === '*' ? null : nodeName; + if ('object' !== typeof ctx) return {nodes:[], pos:[]}; + var nodeList = ctx.nodes.slice(); // TODO: is copy needed? + var r = axes[axis](nodeList /*destructive!*/, nodeTypeNum, nodeName, shouldLowerCase); + return r; + } + } + }; + var evaluate = xpath.evaluate = function evaluate(expr, doc, context) { + //var astFactory = new AstEvaluatorFactory(doc, context); + var stream = new Stream(expr); + var ast = parse(stream, astFactory); + var val = new Evaluator(doc).val(ast, {nodes: [context]}); + return val; + } + + /*************************************************************************** + * DOM interface * + ***************************************************************************/ + var XPathException = xpath.XPathException = function XPathException(code, message) { + var e = new Error(message); + e.name = 'XPathException'; + e.code = code; + return e; + } + XPathException.INVALID_EXPRESSION_ERR = 51; + XPathException.TYPE_ERR = 52; + + + var XPathEvaluator = xpath.XPathEvaluator = function XPathEvaluator() {} + XPathEvaluator.prototype = { + createExpression: function(expression, resolver) { + return new XPathExpression(expression, resolver); + }, + createNSResolver: function(nodeResolver) { + // TODO + }, + evaluate: function evaluate(expression, contextNode, resolver, type, result) { + var expr = new XPathExpression(expression, resolver); + return expr.evaluate(contextNode, type, result); + } + }; + + + var XPathExpression = xpath.XPathExpression = function XPathExpression(expression, resolver, optDoc) { + var stream = new Stream(expression); + this._ast = parse(stream, astFactory); + this._doc = optDoc; + } + XPathExpression.prototype = { + evaluate: function evaluate(contextNode, type, result) { + if (null == contextNode.nodeType) + throw new Error('bad argument (expected context node): ' + contextNode); + var doc = contextNode.ownerDocument || contextNode; + if (null != this._doc && this._doc !== doc) { + throw new core.DOMException( + core.WRONG_DOCUMENT_ERR, + 'The document must be the same as the context node\'s document.'); + } + var evaluator = new Evaluator(doc); + var value = evaluator.val(this._ast, {nodes: [contextNode]}); + if (XPathResult.NUMBER_TYPE === type) + value = fn.number(value); + else if (XPathResult.STRING_TYPE === type) + value = fn.string(value); + else if (XPathResult.BOOLEAN_TYPE === type) + value = fn['boolean'](value); + else if (XPathResult.ANY_TYPE !== type && + XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== type && + XPathResult.ORDERED_NODE_ITERATOR_TYPE !== type && + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== type && + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== type && + XPathResult.ANY_UNORDERED_NODE_TYPE !== type && + XPathResult.FIRST_ORDERED_NODE_TYPE !== type) + throw new core.DOMException( + core.NOT_SUPPORTED_ERR, + 'You must provide an XPath result type (0=any).'); + else if (XPathResult.ANY_TYPE !== type && + 'object' !== typeof value) + throw new XPathException( + XPathException.TYPE_ERR, + 'Value should be a node-set: ' + value); + return new XPathResult(doc, value, type); + } + } + + var XPathResult = xpath.XPathResult = function XPathResult(doc, value, resultType) { + this._value = value; + this._resultType = resultType; + this._i = 0; + this._invalidated = false; + if (this.resultType === XPathResult.UNORDERED_NODE_ITERATOR_TYPE || + this.resultType === XPathResult.ORDERED_NODE_ITERATOR_TYPE) { + doc.addEventListener('DOMSubtreeModified', invalidate, true); + var self = this; + function invalidate() { + self._invalidated = true; + doc.removeEventListener('DOMSubtreeModified', invalidate, true); + } + } + } + XPathResult.ANY_TYPE = 0; + XPathResult.NUMBER_TYPE = 1; + XPathResult.STRING_TYPE = 2; + XPathResult.BOOLEAN_TYPE = 3; + XPathResult.UNORDERED_NODE_ITERATOR_TYPE = 4; + XPathResult.ORDERED_NODE_ITERATOR_TYPE = 5; + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE = 6; + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE = 7; + XPathResult.ANY_UNORDERED_NODE_TYPE = 8; + XPathResult.FIRST_ORDERED_NODE_TYPE = 9; + var proto = { + // XPathResultType + get resultType() { + if (this._resultType) return this._resultType; + switch (typeof this._value) { + case 'number': return XPathResult.NUMBER_TYPE; + case 'string': return XPathResult.STRING_TYPE; + case 'boolean': return XPathResult.BOOLEAN_TYPE; + default: return XPathResult.UNORDERED_NODE_ITERATOR_TYPE; + } + }, + get numberValue() { + if (XPathResult.NUMBER_TYPE !== this.resultType) + throw new XPathException(XPathException.TYPE_ERR, + 'You should have asked for a NUMBER_TYPE.'); + return this._value; + }, + get stringValue() { + if (XPathResult.STRING_TYPE !== this.resultType) + throw new XPathException(XPathException.TYPE_ERR, + 'You should have asked for a STRING_TYPE.'); + return this._value; + }, + get booleanValue() { + if (XPathResult.BOOLEAN_TYPE !== this.resultType) + throw new XPathException(XPathException.TYPE_ERR, + 'You should have asked for a BOOLEAN_TYPE.'); + return this._value; + }, + get singleNodeValue() { + if (XPathResult.ANY_UNORDERED_NODE_TYPE !== this.resultType && + XPathResult.FIRST_ORDERED_NODE_TYPE !== this.resultType) + throw new XPathException( + XPathException.TYPE_ERR, + 'You should have asked for a FIRST_ORDERED_NODE_TYPE.'); + return this._value.nodes[0] || null; + }, + get invalidIteratorState() { + if (XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== this.resultType && + XPathResult.ORDERED_NODE_ITERATOR_TYPE !== this.resultType) + return false; + return !!this._invalidated; + }, + get snapshotLength() { + if (XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== this.resultType && + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== this.resultType) + throw new XPathException( + XPathException.TYPE_ERR, + 'You should have asked for a ORDERED_NODE_SNAPSHOT_TYPE.'); + return this._value.nodes.length; + }, + iterateNext: function iterateNext() { + if (XPathResult.UNORDERED_NODE_ITERATOR_TYPE !== this.resultType && + XPathResult.ORDERED_NODE_ITERATOR_TYPE !== this.resultType) + throw new XPathException( + XPathException.TYPE_ERR, + 'You should have asked for a ORDERED_NODE_ITERATOR_TYPE.'); + if (this.invalidIteratorState) + throw new core.DOMException( + core.INVALID_STATE_ERR, + 'The document has been mutated since the result was returned'); + return this._value.nodes[this._i++] || null; + }, + snapshotItem: function snapshotItem(index) { + if (XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE !== this.resultType && + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE !== this.resultType) + throw new XPathException( + XPathException.TYPE_ERR, + 'You should have asked for a ORDERED_NODE_SNAPSHOT_TYPE.'); + return this._value.nodes[index] || null; + } + }; + // so you can access ANY_TYPE etc. from the instances: + XPathResult.prototype = Object.create(XPathResult, + Object.keys(proto).reduce(function (descriptors, name) { + descriptors[name] = Object.getOwnPropertyDescriptor(proto, name); + return descriptors; + }, { + constructor: { + value: XPathResult, + writable: true, + configurable: true + } + })); + + core.XPathException = XPathException; + core.XPathExpression = XPathExpression; + core.XPathResult = XPathResult; + core.XPathEvaluator = XPathEvaluator; + + core.Document.prototype.createExpression = + XPathEvaluator.prototype.createExpression; + + core.Document.prototype.createNSResolver = + XPathEvaluator.prototype.createNSResolver; + + core.Document.prototype.evaluate = XPathEvaluator.prototype.evaluate; + +})(); diff --git a/node_modules/jsdom/lib/jsdom/living/character-data.js b/node_modules/jsdom/lib/jsdom/living/character-data.js new file mode 100644 index 0000000..89b2eaa --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/character-data.js @@ -0,0 +1,90 @@ +"use strict"; +var inheritFrom = require("../utils").inheritFrom; + +module.exports = function (core) { + core.CharacterData = function CharacterData(ownerDocument, data) { + core.Node.call(this, ownerDocument); + + this._data = data; + }; + + inheritFrom(core.Node, core.CharacterData, { + get data() { return this._data; }, + set data(data) { + if (data === null) { + data = ""; + } + data = String(data); + + this._setData(data); + }, + + get length() { + return this._data.length; + }, + + substringData: function (offset, count) { + offset = offset >>> 0; + count = count >>> 0; + + var length = this.length; + + if (offset > length) { + throw new core.DOMException(core.DOMException.INDEX_SIZE_ERR); + } + + if (offset + count > length) { + return this._data.substring(offset); + } + + return this._data.substring(offset, offset + count); + }, + + appendData: function (data) { + this.replaceData(this.length, 0, data); + }, + + insertData: function (offset, data) { + this.replaceData(offset, 0, data); + }, + + deleteData: function (offset, count) { + this.replaceData(offset, count, ""); + }, + + replaceData: function (offset, count, data) { + offset = offset >>> 0; + count = count >>> 0; + data = String(data); + + var length = this.length; + + if (offset > length) { + throw new core.DOMException(core.DOMException.INDEX_SIZE_ERR); + } + + if (offset + count > length) { + count = length - offset; + } + + var start = this._data.substring(0, offset); + var end = this._data.substring(offset + count); + + this._setData(start + data + end); + + // TODO: range stuff + }, + + _setData: function (newData) { + // TODO: remove this once we no longer rely on mutation events internally, since they are nonstandard + var oldData = this._data; + this._data = newData; + + if (this._ownerDocument && this._parentNode && this._ownerDocument.implementation._hasFeature("MutationEvents")) { + var ev = this._ownerDocument.createEvent("MutationEvents"); + ev.initMutationEvent("DOMCharacterDataModified", true, false, this, oldData, newData, null, null); + this.dispatchEvent(ev); + } + } + }); +}; diff --git a/node_modules/jsdom/lib/jsdom/living/comment.js b/node_modules/jsdom/lib/jsdom/living/comment.js new file mode 100644 index 0000000..471eb5d --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/comment.js @@ -0,0 +1,13 @@ +"use strict"; +var inheritFrom = require("../utils").inheritFrom; + +module.exports = function (core) { + // TODO: constructor should not take ownerDocument + core.Comment = function Comment(ownerDocument, data) { + core.CharacterData.call(this, ownerDocument, data); + }; + + inheritFrom(core.CharacterData, core.Comment, { + nodeType: core.Node.COMMENT_NODE, // TODO should be on prototype, not here + }); +}; diff --git a/node_modules/jsdom/lib/jsdom/living/document-type.js b/node_modules/jsdom/lib/jsdom/living/document-type.js new file mode 100644 index 0000000..abd80aa --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/document-type.js @@ -0,0 +1,19 @@ +"use strict"; +var inheritFrom = require("../utils").inheritFrom; + +module.exports = function (core) { + core.DocumentType = function DocumentType(ownerDocument, name, publicId, systemId) { + core.Node.call(this, ownerDocument); + + this._name = name; + this._publicId = publicId; + this._systemId = systemId; + }; + + inheritFrom(core.Node, core.DocumentType, { + nodeType: core.Node.DOCUMENT_TYPE_NODE, // TODO should be on prototype, not here + get name() { return this._name; }, + get publicId() { return this._publicId; }, + get systemId() { return this._systemId; } + }); +}; diff --git a/node_modules/jsdom/lib/jsdom/living/document.js b/node_modules/jsdom/lib/jsdom/living/document.js new file mode 100644 index 0000000..9943519 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/document.js @@ -0,0 +1,26 @@ +"use strict"; +var validateNames = require("./helpers/validate-names"); + +module.exports = function (core) { + core.Document.prototype.createProcessingInstruction = function (target, data) { + target = String(target); + data = String(data); + + validateNames.name(target, core); + + if (data.indexOf("?>") !== -1) { + throw new core.DOMException(core.DOMException.INVALID_CHARACTER_ERR, + "Processing instruction data cannot contain the string \"?>\""); + } + + return new core.ProcessingInstruction(this._ownerDocument, target, data); + }; + + core.Document.prototype.createTextNode = function (data) { + return new core.Text(this, String(data)); + }; + + core.Document.prototype.createComment = function (data) { + return new core.Comment(this, String(data)); + }; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/dom-implementation.js b/node_modules/jsdom/lib/jsdom/living/dom-implementation.js new file mode 100644 index 0000000..5ca8989 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/dom-implementation.js @@ -0,0 +1,85 @@ +"use strict"; +var validateNames = require("./helpers/validate-names"); + +module.exports = function (core) { + core.DOMImplementation.prototype.hasFeature = function () { + return true; + }; + + core.DOMImplementation.prototype.createDocumentType = function (qualifiedName, publicId, systemId) { + qualifiedName = String(qualifiedName); + publicId = String(publicId); + systemId = String(systemId); + + validateNames.qname(qualifiedName, core); + + return new core.DocumentType(this._ownerDocument, qualifiedName, publicId, systemId); + }; + + core.DOMImplementation.prototype.createDocument = function (namespace, qualifiedName, doctype) { + namespace = namespace !== null ? String(namespace) : namespace; + qualifiedName = qualifiedName === null ? "" : String(qualifiedName); + if (doctype === undefined) { + doctype = null; + } + + var document = new core.Document({ parsingMode: "xml" }); + + var element = null; + if (qualifiedName !== "") { + element = document.createElementNS(namespace, qualifiedName); + } + + if (doctype !== null) { + document.appendChild(doctype); + } + + if (element !== null) { + document.appendChild(element); + } + + return document; + }; + + core.DOMImplementation.prototype.createHTMLDocument = function (title) { + // Let doc be a new document that is an HTML document. + // Set doc's content type to "text/html". + var document = new core.HTMLDocument({ parsingMode: "html" }); + + // Create a doctype, with "html" as its name and with its node document set + // to doc. Append the newly created node to doc. + var doctype = this.createDocumentType("html", "", ""); + document.appendChild(doctype); + + // Create an html element in the HTML namespace, and append it to doc. + var htmlElement = document.createElementNS("http://www.w3.org/1999/xhtml", "html"); + document.appendChild(htmlElement); + + // Create a head element in the HTML namespace, and append it to the html + // element created in the previous step. + var headElement = document.createElement("head"); + htmlElement.appendChild(headElement); + + // If the title argument is not omitted: + if (title !== undefined) { + // Create a title element in the HTML namespace, and append it to the head + // element created in the previous step. + var titleElement = document.createElement("title"); + headElement.appendChild(titleElement); + + // Create a Text node, set its data to title (which could be the empty + // string), and append it to the title element created in the previous step. + titleElement.appendChild(document.createTextNode(title)); + } + + // Create a body element in the HTML namespace, and append it to the html + // element created in the earlier step. + htmlElement.appendChild(document.createElement("body")); + + // doc's origin is an alias to the origin of the context object's associated + // document, and doc's effective script origin is an alias to the effective + // script origin of the context object's associated document. + + return document; + }; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/validate-names.js b/node_modules/jsdom/lib/jsdom/living/helpers/validate-names.js new file mode 100644 index 0000000..5f4622f --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/validate-names.js @@ -0,0 +1,63 @@ +"use strict"; +var xnv = require("xml-name-validator"); + +// https://dom.spec.whatwg.org/#validate + +exports.name = function (name, core) { + try { + xnv.name(name); + } catch (e) { + throw new core.DOMException(core.DOMException.INVALID_CHARACTER_ERR, + "\"" + name + "\" did not match the Name production: " + e.message); + } +}; + +exports.qname = function (qname, core) { + exports.name(qname, core); + + try { + xnv.qname(qname); + } catch (e) { + throw new core.DOMException(core.DOMException.NAMESPACE_ERR, + "\"" + qname + "\" did not match the QName production: " + e.message); + } +}; + +exports.validateAndExtract = function (namespace, qualifiedName, core) { + if (namespace === "") { + namespace = null; + } + + exports.qname(qualifiedName, core); + + var prefix = null; + var localName = qualifiedName; + + var colonIndex = qualifiedName.indexOf(":"); + if (colonIndex !== -1) { + prefix = qualifiedName.substring(0, colonIndex); + localName = qualifiedName.substring(colonIndex + 1); + } + + if (prefix !== null && namespace === null) { + throw new core.DOMException(core.DOMException.NAMESPACE_ERR, + "A namespace was given but a prefix was also extracted from the qualifiedName"); + } + + if (prefix === "xml" && namespace !== "http://www.w3.org/XML/1998/namespace") { + throw new core.DOMException(core.DOMException.NAMESPACE_ERR, + "A prefix of \"xml\" was given but the namespace was not the XML namespace"); + } + + if ((qualifiedName === "xmlns" || prefix === "xmlns") && namespace !== "http://www.w3.org/2000/xmlns/") { + throw new core.DOMException(core.DOMException.NAMESPACE_ERR, + "A prefix or qualifiedName of \"xmlns\" was given but the namespace was not the XMLNS namespace"); + } + + if (namespace === "http://www.w3.org/2000/xmlns/" && qualifiedName !== "xmlns" && prefix !== "xmlns") { + throw new core.DOMException(core.DOMException.NAMESPACE_ERR, + "The XMLNS namespace was given but neither the prefix nor qualifiedName was \"xmlns\""); + } + + return { namespace: namespace, prefix: prefix, localName: localName, qualifiedName: qualifiedName }; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/index.js b/node_modules/jsdom/lib/jsdom/living/index.js new file mode 100644 index 0000000..e4b67a0 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/index.js @@ -0,0 +1,23 @@ +"use strict"; +var core = module.exports = require("../level1/core"); + +// These (because of how they were written) directly include level1/core and modify it. +// ORDER IS IMPORTANT +require("../level2/core"); +require("../level2/events"); +require("../level2/html"); +require("../level2/style"); +require("../level3/core"); +require("../level3/ls"); +require("../level3/xpath"); + +require("./document-type")(core); +require("./character-data")(core); +require("./processing-instruction")(core); +require("./comment")(core); +require("./text")(core); +require("./dom-implementation")(core); +require("./document")(core); +require("./node-filter")(core); +require("./node")(core); +require("./selectors")(core); diff --git a/node_modules/jsdom/lib/jsdom/living/node-filter.js b/node_modules/jsdom/lib/jsdom/living/node-filter.js new file mode 100644 index 0000000..f945b84 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/node-filter.js @@ -0,0 +1,47 @@ +"use strict"; +var addConstants = require("../utils").addConstants; + +module.exports = function (core) { + // https://dom.spec.whatwg.org/#interface-nodefilter + core.NodeFilter = function () { + throw new TypeError("Illegal constructor"); + }; + + /** + * Returns an unsigned short that will be used to tell if a given Node must + * be accepted or not by the NodeIterator or TreeWalker iteration + * algorithm. This method is expected to be written by the user of a + * NodeFilter. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter + * @interface + * + * @param {Node} node DOM Node + * @return {FILTER_ACCEPT|FILTER_REJECT|FILTER_SKIP} + */ + core.NodeFilter.acceptNode = function(/* node */) { + throw new Error("This method is expected to be written by the user of a NodeFilter."); + }; + + addConstants(core.NodeFilter, { + // Constants for whatToShow + SHOW_ALL : 0xFFFFFFFF, + SHOW_ELEMENT : 0x00000001, + SHOW_ATTRIBUTE : 0x00000002, + SHOW_TEXT : 0x00000004, + SHOW_CDATA_SECTION : 0x00000008, + SHOW_ENTITY_REFERENCE : 0x00000010, + SHOW_ENTITY : 0x00000020, + SHOW_PROCESSING_INSTRUCTION : 0x00000040, + SHOW_COMMENT : 0x00000080, + SHOW_DOCUMENT : 0x00000100, + SHOW_DOCUMENT_TYPE : 0x00000200, + SHOW_DOCUMENT_FRAGMENT : 0x00000400, + SHOW_NOTATION : 0x00000800, + + // Constants returned by acceptNode + FILTER_ACCEPT : 1, + FILTER_REJECT : 2, + FILTER_SKIP : 3 + }); +}; diff --git a/node_modules/jsdom/lib/jsdom/living/node.js b/node_modules/jsdom/lib/jsdom/living/node.js new file mode 100644 index 0000000..0ff38f9 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/node.js @@ -0,0 +1,261 @@ +"use strict"; +var defineGetter = require("../utils").defineGetter; + +module.exports = function (core) { + var DOCUMENT_POSITION_DISCONNECTED = core.Node.DOCUMENT_POSITION_DISCONNECTED; + var DOCUMENT_POSITION_PRECEDING = core.Node.DOCUMENT_POSITION_PRECEDING; + var DOCUMENT_POSITION_FOLLOWING = core.Node.DOCUMENT_POSITION_FOLLOWING; + var DOCUMENT_POSITION_CONTAINS = core.Node.DOCUMENT_POSITION_CONTAINS; + var DOCUMENT_POSITION_CONTAINED_BY = core.Node.DOCUMENT_POSITION_CONTAINED_BY; + var DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = core.Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC; + + /** + * Return true if node is of a type obsoleted by the WHATWG living standard + * @param {Node} node + * @return {Boolean} + */ + function isObsoleteNodeType(node) { + return node.nodeType === core.Node.ENTITY_NODE || + node.nodeType === core.Node.ENTITY_REFERENCE_NODE || + node.nodeType === core.Node.NOTATION_NODE || + node.nodeType === core.Node.CDATA_SECTION_NODE; + } + + /** + * Return the parent node of node, whatever its nodeType is + * @param {Node} node + * @return {Node or null} + */ + function getNodeParent(node) { + + if(!node) { + return node; + } + + switch (node.nodeType) { + + case core.Node.DOCUMENT_NODE: + case core.Node.DOCUMENT_FRAGMENT_NODE: + return null; + + case core.Node.COMMENT_NODE: + case core.Node.DOCUMENT_TYPE_NODE: + case core.Node.ELEMENT_NODE: + case core.Node.PROCESSING_INSTRUCTION_NODE: + case core.Node.TEXT_NODE: + return node.parentNode; + + case core.Node.ATTRIBUTE_NODE: + + return node._parentNode; + + default: + throw new Error("Unknown node type:" + node.nodeType); + } + } + + /** + * Walk up the node tree and return the nodes root node + * @param {Node} node + * @return {Node} + */ + function findNodeRoot(node) { + if (!getNodeParent(node)) { + return node; + } + + return findNodeRoot(getNodeParent(node)); + } + + /** + * Walk up the node tree returning true if otherNode is an ancestor of node + * @param {Node} node + * @param {Node} otherNode + * @return {Boolean} + */ + function isAncestor(node, otherNode) { + var parentNode = node.nodeType === node.ATTRIBUTE_NODE ? node._parentNode : node.parentNode; + + if (!parentNode) { + return false; + } + + if(parentNode === otherNode) { + return true; + } + + return isAncestor(parentNode, otherNode); + } + + /** + * Traverse the node tree starting at current. Return DOCUMENT_POSITION_FOLLOWING if otherNode follows node. Return + * DOCUMENT_POSITION_PRECEDING if otherNode precedes node + * @param {Node} current + * @param {Node} node + * @param {Node} otherNode + * @return {Number} + */ + function followingOrPreceding(current, node, otherNode) { + if (current === node) { + return core.Node.DOCUMENT_POSITION_FOLLOWING; + } + + if (current === otherNode) { + return core.Node.DOCUMENT_POSITION_PRECEDING; + } + + var i = 0, len = current._childNodes.length, child, result; + + for(; i < len; i += 1) { + + child = current._childNodes[i]; + + if((result = followingOrPreceding(child, node, otherNode)) !== 0) { + return result; + } + } + + return 0; + } + + /** + * Returns a bitmask Number composed of DOCUMENT_POSITION constants based upon the rules defined in + * http://dom.spec.whatwg.org/#dom-node-comparedocumentposition + * @param {Node} other + * @return {Number} + */ + core.Node.prototype.compareDocumentPosition = function compareDocumentPosition (other) { + // Let reference be the context object. + var reference = this; + + if(!(other instanceof core.Node)) { + throw Error("Comparing position against non-Node values is not allowed"); + } + + if (isObsoleteNodeType(reference) || isObsoleteNodeType(other)) { + throw new Error("Obsolete node type"); + } + + // If other and reference are the same object, return zero. + if (reference === other) { + return 0; + } + + // If other and reference are not in the same tree, return the result of adding DOCUMENT_POSITION_DISCONNECTED, + // DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING, + // with the constraint that this is to be consistent, together. + if (findNodeRoot(reference) !== findNodeRoot(other)) { + return DOCUMENT_POSITION_DISCONNECTED + DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC + DOCUMENT_POSITION_FOLLOWING; + } + + // If other is an ancestor of reference, return the result of adding DOCUMENT_POSITION_CONTAINS to + // DOCUMENT_POSITION_PRECEDING. + if (isAncestor(reference, other)) { + return DOCUMENT_POSITION_CONTAINS + DOCUMENT_POSITION_PRECEDING; + } + + // If other is a descendant of reference, return the result of adding DOCUMENT_POSITION_CONTAINED_BY to + // DOCUMENT_POSITION_FOLLOWING. + if (isAncestor(other, reference)) { + return DOCUMENT_POSITION_CONTAINED_BY + DOCUMENT_POSITION_FOLLOWING; + } + + // If other is preceding reference return DOCUMENT_POSITION_PRECEDING, otherwise return DOCUMENT_POSITION_FOLLOWING + return followingOrPreceding(findNodeRoot(reference), reference, other); + }; + + /** + * The contains(other) method returns true if other is an inclusive descendant of the context object, + * and false otherwise (including when other is null). + * @param {[Node]} other [the node to test] + * @return {[boolean]} [whether other is an inclusive descendant of this] + */ + core.Node.prototype.contains = function (other) { + return other instanceof core.Node && + (this === other || this.compareDocumentPosition(other) & DOCUMENT_POSITION_CONTAINED_BY); + }; + + // http://dom.spec.whatwg.org/#dom-node-parentelement + defineGetter(core.Node.prototype, "parentElement", function () { + return this._parentNode !== null && this._parentNode.nodeType === core.Node.ELEMENT_NODE ? this._parentNode : null; + }); + + function nodeEquals(a, b) { + if (a.nodeType !== b.nodeType) { + return false; + } + + switch (a.nodeType) { + case core.Node.DOCUMENT_TYPE_NODE: + if (a._name !== b._name || a._publicId !== b._publicId || a._systemId !== b._systemId) { + return false; + } + break; + case core.Node.ELEMENT_NODE: + if (a._namespaceURI !== b._namespaceURI || a._prefix !== b._prefix || a._localName !== b._localName || + a._attributes.length !== b._attributes.length) { + return false; + } + break; + case core.Node.PROCESSING_INSTRUCTION_NODE: + if (a._target !== b._target || a._data !== b._data) { + return false; + } + break; + case core.Node.TEXT_NODE: + case core.Node.COMMENT_NODE: + if (a._data !== b._data) { + return false; + } + break; + } + + if (a.nodeType === core.Node.ELEMENT_NODE) { + for (var i = 0; i < a._attributes.length; ++i) { + var aAttr = a._attributes[i]; + var bAttr = b._attributes.$getNode(aAttr._namespaceURI, aAttr._localName); + if (!bAttr) { + return false; + } + + // TODO: once Attr is simplified, check the internal property instead of the public getter + if (aAttr.value !== bAttr.value) { + return false; + } + } + } + + if (a._childNodes.length !== b._childNodes.length) { + return false; + } + + for (var j = 0; j < a._childNodes.length; ++j) { + if (!nodeEquals(a._childNodes[j], b._childNodes[j])) { + return false; + } + } + + return true; + } + + // https://dom.spec.whatwg.org/#dom-node-isequalnode + core.Node.prototype.isEqualNode = function (node) { + if (node === undefined) { + // this is what Node? means in the IDL + node = null; + } + + if (node === null) { + return false; + } + + // Fast-path, not in the spec + if (this === node) { + return true; + } + + return nodeEquals(this, node); + }; +}; + + diff --git a/node_modules/jsdom/lib/jsdom/living/processing-instruction.js b/node_modules/jsdom/lib/jsdom/living/processing-instruction.js new file mode 100644 index 0000000..ce9c4b7 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/processing-instruction.js @@ -0,0 +1,15 @@ +"use strict"; +var inheritFrom = require("../utils").inheritFrom; + +module.exports = function (core) { + core.ProcessingInstruction = function ProcessingInstruction(ownerDocument, target, data) { + core.CharacterData.call(this, ownerDocument, data); + + this._target = target; + }; + + inheritFrom(core.CharacterData, core.ProcessingInstruction, { + nodeType: core.Node.PROCESSING_INSTRUCTION_NODE, // TODO should be on prototype, not here + get target() { return this._target; } + }); +}; diff --git a/node_modules/jsdom/lib/jsdom/living/selectors.js b/node_modules/jsdom/lib/jsdom/living/selectors.js new file mode 100644 index 0000000..d159f5a --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/selectors.js @@ -0,0 +1,32 @@ +"use strict"; +var nwmatcher = require("nwmatcher/src/nwmatcher-noqsa"); +var memoizeQuery = require("../utils").memoizeQuery; + +module.exports = function (core) { + [core.Document, core.DocumentFragment, core.Element].forEach(function (Class) { + Class.prototype.querySelector = memoizeQuery(function (selectors) { + return addNwmatcher(this).first(String(selectors), this); + }); + + Class.prototype.querySelectorAll = memoizeQuery(function (selectors) { + return new core.NodeList(addNwmatcher(this).select(String(selectors), this)); + }); + }); + + core.Element.prototype.matches = memoizeQuery(function (selectors) { + return addNwmatcher(this).match(this, selectors); + }); +}; + +// nwmatcher gets `document.documentElement` at creation-time, so we have to initialize lazily, since in the initial +// stages of Document initialization, there is no documentElement present yet. +function addNwmatcher(parentNode) { + var document = parentNode._ownerDocument; + + if (!document._nwmatcher) { + document._nwmatcher = nwmatcher({ document: document }); + document._nwmatcher.configure({ UNIQUE_ID: false }); + } + + return document._nwmatcher; +} diff --git a/node_modules/jsdom/lib/jsdom/living/text.js b/node_modules/jsdom/lib/jsdom/living/text.js new file mode 100644 index 0000000..ecd8c63 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/text.js @@ -0,0 +1,41 @@ +"use strict"; +var inheritFrom = require("../utils").inheritFrom; + +module.exports = function (core) { + // TODO: constructor should not take ownerDocument + core.Text = function Text(ownerDocument, data) { + core.CharacterData.call(this, ownerDocument, data); + }; + + inheritFrom(core.CharacterData, core.Text, { + nodeType: core.Node.TEXT_NODE, // TODO should be on prototype, not here + splitText: function (offset) { + offset = offset >>> 0; + + var length = this.length; + + if (offset > length) { + throw new core.DOMException(core.DOMException.INDEX_SIZE_ERR); + } + + var count = length - offset; + var newData = this.substringData(offset, count); + + var newNode = this._ownerDocument.createTextNode(newData); + + var parent = this._parentNode; + + if (parent !== null) { + parent.insertBefore(newNode, this.nextSibling); + } + + this.replaceData(offset, count, ""); + + return newNode; + + // TODO: range stuff + }, + + // TODO: wholeText property + }); +}; diff --git a/node_modules/jsdom/lib/jsdom/utils.js b/node_modules/jsdom/lib/jsdom/utils.js new file mode 100644 index 0000000..1894611 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/utils.js @@ -0,0 +1,223 @@ +"use strict"; +var path = require("path"); +var url = require("url"); + +exports.toFileUrl = function (fileName) { + // Beyond just the `path.resolve`, this is mostly for the benefit of Windows, + // where we need to convert "\" to "/" and add an extra "/" prefix before the + // drive letter. + var pathname = path.resolve(process.cwd(), fileName).replace(/\\/g, "/"); + if (pathname[0] !== "/") { + pathname = "/" + pathname; + } + + return "file://" + pathname; +}; + +/** + * Define a setter on an object + * + * This method replaces any existing setter but leaves getters in place. + * + * - `object` {Object} the object to define the setter on + * - `property` {String} the name of the setter + * - `setterFn` {Function} the setter + */ +exports.defineSetter = function defineSetter(object, property, setterFn) { + var descriptor = Object.getOwnPropertyDescriptor(object, property) || { + configurable: true, + enumerable: true + }; + + descriptor.set = setterFn; + + Object.defineProperty(object, property, descriptor); +}; + +/** + * Define a getter on an object + * + * This method replaces any existing getter but leaves setters in place. + * + * - `object` {Object} the object to define the getter on + * - `property` {String} the name of the getter + * - `getterFn` {Function} the getter + */ +exports.defineGetter = function defineGetter(object, property, getterFn) { + var descriptor = Object.getOwnPropertyDescriptor(object, property) || { + configurable: true, + enumerable: true + }; + + descriptor.get = getterFn; + + Object.defineProperty(object, property, descriptor); +}; + +/** + * Create an object with the given prototype + * + * Optionally augment the created object. + * + * - `prototype` {Object} the created object's prototype + * - `[properties]` {Object} properties to attach to the created object + */ +exports.createFrom = function createFrom(prototype, properties) { + properties = properties || {}; + + var descriptors = {}; + Object.getOwnPropertyNames(properties).forEach(function (name) { + descriptors[name] = Object.getOwnPropertyDescriptor(properties, name); + }); + + return Object.create(prototype, descriptors); +}; + +/** + * Create an inheritance relationship between two classes + * + * Optionally augment the inherited prototype. + * + * - `Superclass` {Function} the inherited class + * - `Subclass` {Function} the inheriting class + * - `[properties]` {Object} properties to attach to the inherited prototype + */ +exports.inheritFrom = function inheritFrom(Superclass, Subclass, properties) { + properties = properties || {}; + + Object.defineProperty(properties, "constructor", { + value: Subclass, + writable: true, + configurable: true + }); + + Subclass.prototype = exports.createFrom(Superclass.prototype, properties); +}; + +/** + * Define a list of constants on a constructor and its .prototype + * + * - `Constructor` {Function} the constructor to define the constants on + * - `propertyMap` {Object} key/value map of properties to define + */ +exports.addConstants = function addConstants(Constructor, propertyMap) { + for (var property in propertyMap) { + var value = propertyMap[property]; + addConstant(Constructor, property, value); + addConstant(Constructor.prototype, property, value); + } +}; + +function addConstant(object, property, value) { + Object.defineProperty(object, property, { + configurable: false, + enumerable: true, + value: value, + writable: false + }); +} + +var memoizeQueryTypeCounter = 0; + +/** + * Returns a version of a method that memoizes specific types of calls on the object + * + * - `fn` {Function} the method to be memozied + */ +exports.memoizeQuery = function memoizeQuery(fn) { + // Only memoize query functions with arity <= 2 + if (fn.length > 2) { + return fn; + } + + var type = memoizeQueryTypeCounter++; + + return function () { + if (!this._memoizedQueries) { + return fn.apply(this, arguments); + } + + if (!this._memoizedQueries[type]) { + this._memoizedQueries[type] = Object.create(null); + } + + var key; + if (arguments.length === 1 && typeof arguments[0] === "string") { + key = arguments[0]; + } else if (arguments.length === 2 && typeof arguments[0] === "string" && typeof arguments[1] === "string") { + key = arguments[0] + "::" + arguments[1]; + } else { + return fn.apply(this, arguments); + } + + if (!(key in this._memoizedQueries[type])) { + this._memoizedQueries[type][key] = fn.apply(this, arguments); + } + return this._memoizedQueries[type][key]; + }; +}; + +/** +* A slightly-more-compliant version of `url.resolve`, taking care of a few Node bugs. +*/ +exports.resolveHref = function resolveHref(baseUrl, href) { + var about = "about:blank"; + + // if we're redirecting to another site on about:blank, just let it through + if (href.substring(0, about.length) === about) { + return href; + } + + // if we have to resolve from about:blank we need a bit of special logic + if (baseUrl.substring(0, about.length) === about) { + if (href.search(/^([A-Za-z0-9]+):/) === 0) { // we have an absolute url + baseUrl = href; + href = ""; + } else if (href[0] === "#") { // we have a location hash on about:blank + return baseUrl.split(/#/)[0] + href; + } else if (!href) { // we just have some base url + return baseUrl; + } else { // must be a file url + baseUrl = "file://" + href; + href = ""; + } + } + + if (baseUrl === resolveHref.memoizedUrl && resolveHref.cache && resolveHref.cache[href]) { + return resolveHref.cache[href]; + } + + // When switching protocols, the path doesn't get canonicalized (i.e. .s and ..s are still left): + // https://github.com/joyent/node/issues/5453 + var intermediate = url.resolve(baseUrl, href); + + // This canonicalizes the path, at the cost of overwriting the hash. + var nextStep = url.resolve(intermediate, "#"); + + // But it breaks file URLs by removing their colon O_o, so put that back. + nextStep = nextStep.replace(/^file:\/\/([a-z])\//i, "file:///$1:/"); + + // So, insert the hash back in, if there was one. + var parsed = url.parse(intermediate); + var fixed = nextStep.slice(0, -1) + (parsed.hash || ""); + + // Finally, fix file:/// URLs on Windows, where Node removes their drive letters: + // https://github.com/joyent/node/issues/5452 + if (/^file\:\/\/\/[a-z]\:\//i.test(baseUrl) && /^file\:\/\/\//.test(fixed) && + !/^file\:\/\/\/[a-z]\:\//i.test(fixed)) { + fixed = fixed.replace(/^file\:\/\/\//, baseUrl.substring(0, 11)); + } + + // HORRIBLE HACK: encode \u00E4 correctly just so that we pass + // https://github.com/w3c/web-platform-tests/blob/e75f01a689a3481f5c773315c2c2527712cf8c2c/dom/nodes/DOMImplementation-createHTMLDocument.html#L71-L72 + // Eventually we should replace this with a real URL parser based on the URL standard. + fixed = fixed.replace(/\u00E4/, "%C3%A4"); + + if (baseUrl !== resolveHref.memoizedUrl) { + resolveHref.memoizedUrl = baseUrl; + resolveHref.cache = {}; + } + resolveHref.cache[href] = fixed; + + return fixed; +}; diff --git a/node_modules/jsdom/lib/jsdom/virtual-console.js b/node_modules/jsdom/lib/jsdom/virtual-console.js new file mode 100644 index 0000000..1cc7723 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/virtual-console.js @@ -0,0 +1,24 @@ +"use strict"; + +var EventEmitter = require("events").EventEmitter; +var utils = require("./utils"); + +function VirtualConsole() { + // If "error" event has no listeners, + // EventEmitter throws an exception + this.on("error", function () {}); +} + +utils.inheritFrom(EventEmitter, VirtualConsole, { + sendTo: function (anyConsole) { + Object.keys(anyConsole).forEach(function (method) { + if (typeof anyConsole[method] === "function") { + this.on(method, function () { + anyConsole[method].apply(anyConsole, arguments); + }); + } + }, this); + } +}); + +module.exports = VirtualConsole; |
