blob: 26c84846f76ab2b0fe09ed8f38afd3807cb7e052 [file] [log] [blame]
/*
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);
}
});