@xmldom/xmldom
Version:
A pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module.
1,449 lines (1,409 loc) • 125 kB
JavaScript
'use strict';
var conventions = require('./conventions');
var find = conventions.find;
var hasDefaultHTMLNamespace = conventions.hasDefaultHTMLNamespace;
var hasOwn = conventions.hasOwn;
var isHTMLMimeType = conventions.isHTMLMimeType;
var isHTMLRawTextElement = conventions.isHTMLRawTextElement;
var isHTMLVoidElement = conventions.isHTMLVoidElement;
var MIME_TYPE = conventions.MIME_TYPE;
var NAMESPACE = conventions.NAMESPACE;
/**
* Private DOM Constructor symbol
*
* Internal symbol used for construction of all classes whose constructors should be private.
* Currently used for checks in `Node`, `Document`, `Element`, `Attr`, `CharacterData`, `Text`, `Comment`,
* `CDATASection`, `DocumentType`, `Notation`, `Entity`, `EntityReference`, `DocumentFragment`, `ProcessingInstruction`
* so the constructor can't be used from outside the module.
*/
var PDC = Symbol();
var errors = require('./errors');
var DOMException = errors.DOMException;
var DOMExceptionName = errors.DOMExceptionName;
var g = require('./grammar');
/**
* Checks if the given symbol equals the Private DOM Constructor symbol (PDC)
* and throws an Illegal constructor exception when the symbols don't match.
* This ensures that the constructor remains private and can't be used outside this module.
*/
function checkSymbol(symbol) {
if (symbol !== PDC) {
throw new TypeError('Illegal constructor');
}
}
/**
* A prerequisite for `[].filter`, to drop elements that are empty.
*
* @param {string} input
* The string to be checked.
* @returns {boolean}
* Returns `true` if the input string is not empty, `false` otherwise.
*/
function notEmptyString(input) {
return input !== '';
}
/**
* Splits a string on ASCII whitespace characters (U+0009 TAB, U+000A LF, U+000C FF, U+000D CR,
* U+0020 SPACE).
* It follows the definition from the infra specification from WHATWG.
*
* @param {string} input
* The string to be split.
* @returns {string[]}
* An array of the split strings. The array can be empty if the input string is empty or only
* contains whitespace characters.
* @see {@link https://infra.spec.whatwg.org/#split-on-ascii-whitespace}
* @see {@link https://infra.spec.whatwg.org/#ascii-whitespace}
*/
function splitOnASCIIWhitespace(input) {
// U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, U+0020 SPACE
return input ? input.split(/[\t\n\f\r ]+/).filter(notEmptyString) : [];
}
/**
* Adds element as a key to current if it is not already present.
*
* @param {Record<string, boolean | undefined>} current
* The current record object to which the element will be added as a key.
* The object's keys are string types and values are either boolean or undefined.
* @param {string} element
* The string to be added as a key to the current record.
* @returns {Record<string, boolean | undefined>}
* The updated record object after the addition of the new element.
*/
function orderedSetReducer(current, element) {
if (!hasOwn(current, element)) {
current[element] = true;
}
return current;
}
/**
* Converts a string into an ordered set by splitting the input on ASCII whitespace and
* ensuring uniqueness of elements.
* This follows the definition of an ordered set from the infra specification by WHATWG.
*
* @param {string} input
* The input string to be transformed into an ordered set.
* @returns {string[]}
* An array of unique strings obtained from the input, preserving the original order.
* The array can be empty if the input string is empty or only contains whitespace characters.
* @see {@link https://infra.spec.whatwg.org/#ordered-set}
*/
function toOrderedSet(input) {
if (!input) return [];
var list = splitOnASCIIWhitespace(input);
return Object.keys(list.reduce(orderedSetReducer, {}));
}
/**
* Uses `list.indexOf` to implement a function that behaves like `Array.prototype.includes`.
* This function is used in environments where `Array.prototype.includes` may not be available.
*
* @param {any[]} list
* The array in which to search for the element.
* @returns {function(any): boolean}
* A function that accepts an element and returns a boolean indicating whether the element is
* included in the provided list.
*/
function arrayIncludes(list) {
return function (element) {
return list && list.indexOf(element) !== -1;
};
}
/**
* Validates a qualified name based on the criteria provided in the DOM specification by
* WHATWG.
*
* @param {string} qualifiedName
* The qualified name to be validated.
* @throws {DOMException}
* With code {@link DOMException.INVALID_CHARACTER_ERR} if the qualified name contains an
* invalid character.
* @see {@link https://dom.spec.whatwg.org/#validate}
*/
function validateQualifiedName(qualifiedName) {
if (!g.QName_exact.test(qualifiedName)) {
throw new DOMException(DOMException.INVALID_CHARACTER_ERR, 'invalid character in qualified name "' + qualifiedName + '"');
}
}
/**
* Validates a qualified name and the namespace associated with it,
* based on the criteria provided in the DOM specification by WHATWG.
*
* @param {string | null} namespace
* The namespace to be validated. It can be a string or null.
* @param {string} qualifiedName
* The qualified name to be validated.
* @returns {[namespace: string | null, prefix: string | null, localName: string]}
* Returns a tuple with the namespace,
* prefix and local name of the qualified name.
* @throws {DOMException}
* Throws a DOMException if the qualified name or the namespace is not valid.
* @see {@link https://dom.spec.whatwg.org/#validate-and-extract}
*/
function validateAndExtract(namespace, qualifiedName) {
validateQualifiedName(qualifiedName);
namespace = namespace || null;
/**
* @type {string | null}
*/
var prefix = null;
var localName = qualifiedName;
if (qualifiedName.indexOf(':') >= 0) {
var splitResult = qualifiedName.split(':');
prefix = splitResult[0];
localName = splitResult[1];
}
if (prefix !== null && namespace === null) {
throw new DOMException(DOMException.NAMESPACE_ERR, 'prefix is non-null and namespace is null');
}
if (prefix === 'xml' && namespace !== conventions.NAMESPACE.XML) {
throw new DOMException(DOMException.NAMESPACE_ERR, 'prefix is "xml" and namespace is not the XML namespace');
}
if ((prefix === 'xmlns' || qualifiedName === 'xmlns') && namespace !== conventions.NAMESPACE.XMLNS) {
throw new DOMException(
DOMException.NAMESPACE_ERR,
'either qualifiedName or prefix is "xmlns" and namespace is not the XMLNS namespace'
);
}
if (namespace === conventions.NAMESPACE.XMLNS && prefix !== 'xmlns' && qualifiedName !== 'xmlns') {
throw new DOMException(
DOMException.NAMESPACE_ERR,
'namespace is the XMLNS namespace and neither qualifiedName nor prefix is "xmlns"'
);
}
return [namespace, prefix, localName];
}
/**
* Copies properties from one object to another.
* It only copies the object's own (not inherited) properties.
*
* @param {Object} src
* The source object from which properties are copied.
* @param {Object} dest
* The destination object to which properties are copied.
*/
function copy(src, dest) {
for (var p in src) {
if (hasOwn(src, p)) {
dest[p] = src[p];
}
}
}
/**
* Extends a class with the properties and methods of a super class.
* It uses a form of prototypal inheritance, and establishes the `constructor` property
* correctly(?).
*
* It is not clear to the current maintainers if this implementation is making sense,
* since it creates an intermediate prototype function,
* which all properties of `Super` are copied onto using `_copy`.
*
* @param {Object} Class
* The class that is to be extended.
* @param {Object} Super
* The super class from which properties and methods are inherited.
* @private
*/
function _extends(Class, Super) {
var pt = Class.prototype;
if (!(pt instanceof Super)) {
function t() {}
t.prototype = Super.prototype;
t = new t();
copy(pt, t);
Class.prototype = pt = t;
}
if (pt.constructor != Class) {
if (typeof Class != 'function') {
console.error('unknown Class:' + Class);
}
pt.constructor = Class;
}
}
var NodeType = {};
var ELEMENT_NODE = (NodeType.ELEMENT_NODE = 1);
var ATTRIBUTE_NODE = (NodeType.ATTRIBUTE_NODE = 2);
var TEXT_NODE = (NodeType.TEXT_NODE = 3);
var CDATA_SECTION_NODE = (NodeType.CDATA_SECTION_NODE = 4);
var ENTITY_REFERENCE_NODE = (NodeType.ENTITY_REFERENCE_NODE = 5);
var ENTITY_NODE = (NodeType.ENTITY_NODE = 6);
var PROCESSING_INSTRUCTION_NODE = (NodeType.PROCESSING_INSTRUCTION_NODE = 7);
var COMMENT_NODE = (NodeType.COMMENT_NODE = 8);
var DOCUMENT_NODE = (NodeType.DOCUMENT_NODE = 9);
var DOCUMENT_TYPE_NODE = (NodeType.DOCUMENT_TYPE_NODE = 10);
var DOCUMENT_FRAGMENT_NODE = (NodeType.DOCUMENT_FRAGMENT_NODE = 11);
var NOTATION_NODE = (NodeType.NOTATION_NODE = 12);
var DocumentPosition = conventions.freeze({
DOCUMENT_POSITION_DISCONNECTED: 1,
DOCUMENT_POSITION_PRECEDING: 2,
DOCUMENT_POSITION_FOLLOWING: 4,
DOCUMENT_POSITION_CONTAINS: 8,
DOCUMENT_POSITION_CONTAINED_BY: 16,
DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32,
});
//helper functions for compareDocumentPosition
/**
* Finds the common ancestor in two parent chains.
*
* @param {Node[]} a
* The first parent chain.
* @param {Node[]} b
* The second parent chain.
* @returns {Node}
* The common ancestor node if it exists. If there is no common ancestor, the function will
* return `null`.
*/
function commonAncestor(a, b) {
if (b.length < a.length) return commonAncestor(b, a);
var c = null;
for (var n in a) {
if (a[n] !== b[n]) return c;
c = a[n];
}
return c;
}
/**
* Assigns a unique identifier to a document to ensure consistency while comparing unrelated
* nodes.
*
* @param {Document} doc
* The document to which a unique identifier is to be assigned.
* @returns {string}
* The unique identifier of the document. If the document already had a unique identifier, the
* function will return the existing one.
*/
function docGUID(doc) {
if (!doc.guid) doc.guid = Math.random();
return doc.guid;
}
//-- end of helper functions
/**
* The NodeList interface provides the abstraction of an ordered collection of nodes,
* without defining or constraining how this collection is implemented.
* NodeList objects in the DOM are live.
* The items in the NodeList are accessible via an integral index, starting from 0.
* You can also access the items of the NodeList with a `for...of` loop.
*
* @class NodeList
* @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-536297177
* @constructs NodeList
*/
function NodeList() {}
NodeList.prototype = {
/**
* The number of nodes in the list. The range of valid child node indices is 0 to length-1
* inclusive.
*
* @type {number}
*/
length: 0,
/**
* Returns the item at `index`. If index is greater than or equal to the number of nodes in
* the list, this returns null.
*
* @param index
* Unsigned long Index into the collection.
* @returns {Node | null}
* The node at position `index` in the NodeList,
* or null if that is not a valid index.
*/
item: function (index) {
return index >= 0 && index < this.length ? this[index] : null;
},
/**
* Returns a string representation of the NodeList.
*
* Accepts the same `options` object as `XMLSerializer.prototype.serializeToString`
* (`requireWellFormed`, `splitCDATASections`, `nodeFilter`). Passing a function is treated as
* a legacy `nodeFilter` for backward compatibility.
*
* @param {Object | function} [options]
* @param {boolean} [options.requireWellFormed=false]
* @param {boolean} [options.splitCDATASections=true]
* @param {function} [options.nodeFilter]
* @returns {string}
*/
toString: function (options) {
var opts;
if (typeof options === 'function') {
opts = { requireWellFormed: false, splitCDATASections: true, nodeFilter: options };
} else if (!!options) {
opts = {
requireWellFormed: !!options.requireWellFormed,
splitCDATASections: options.splitCDATASections !== false,
nodeFilter: options.nodeFilter || null,
};
} else {
opts = { requireWellFormed: false, splitCDATASections: true, nodeFilter: null };
}
for (var buf = [], i = 0; i < this.length; i++) {
serializeToString(this[i], buf, null, opts);
}
return buf.join('');
},
/**
* Filters the NodeList based on a predicate.
*
* @param {function(Node): boolean} predicate
* - A predicate function to filter the NodeList.
* @returns {Node[]}
* An array of nodes that satisfy the predicate.
* @private
*/
filter: function (predicate) {
return Array.prototype.filter.call(this, predicate);
},
/**
* Returns the first index at which a given node can be found in the NodeList, or -1 if it is
* not present.
*
* @param {Node} item
* - The Node item to locate in the NodeList.
* @returns {number}
* The first index of the node in the NodeList; -1 if not found.
* @private
*/
indexOf: function (item) {
return Array.prototype.indexOf.call(this, item);
},
};
NodeList.prototype[Symbol.iterator] = function () {
var me = this;
var index = 0;
return {
next: function () {
if (index < me.length) {
return {
value: me[index++],
done: false,
};
} else {
return {
done: true,
};
}
},
return: function () {
return {
done: true,
};
},
};
};
/**
* Represents a live collection of nodes that is automatically updated when its associated
* document changes.
*
* @class LiveNodeList
* @param {Node} node
* The associated node.
* @param {function} refresh
* The function to refresh the live node list.
* @augments NodeList
* @constructs LiveNodeList
*/
function LiveNodeList(node, refresh) {
this._node = node;
this._refresh = refresh;
_updateLiveList(this);
}
/**
* Updates the live node list.
*
* @param {LiveNodeList} list
* The live node list to update.
* @private
*/
function _updateLiveList(list) {
var inc = list._node._inc || list._node.ownerDocument._inc;
if (list._inc !== inc) {
var ls = list._refresh(list._node);
__set__(list, 'length', ls.length);
if (!list.$$length || ls.length < list.$$length) {
for (var i = ls.length; i in list; i++) {
if (hasOwn(list, i)) {
delete list[i];
}
}
}
copy(ls, list);
list._inc = inc;
}
}
/**
* Returns the node at position `index` in the LiveNodeList, or null if that is not a valid
* index.
*
* @param {number} i
* Index into the collection.
* @returns {Node | null}
* The node at position `index` in the LiveNodeList, or null if that is not a valid index.
*/
LiveNodeList.prototype.item = function (i) {
_updateLiveList(this);
return this[i] || null;
};
_extends(LiveNodeList, NodeList);
/**
* Objects implementing the NamedNodeMap interface are used to represent collections of nodes
* that can be accessed by name.
* Note that NamedNodeMap does not inherit from NodeList;
* NamedNodeMaps are not maintained in any particular order.
* Objects contained in an object implementing NamedNodeMap may also be accessed by an ordinal
* index,
* but this is simply to allow convenient enumeration of the contents of a NamedNodeMap,
* and does not imply that the DOM specifies an order to these Nodes.
* NamedNodeMap objects in the DOM are live.
* used for attributes or DocumentType entities
*
* This implementation only supports property indices, but does not support named properties,
* as specified in the living standard.
*
* @class NamedNodeMap
* @see https://dom.spec.whatwg.org/#interface-namednodemap
* @see https://webidl.spec.whatwg.org/#dfn-supported-property-names
* @constructs NamedNodeMap
*/
function NamedNodeMap() {}
/**
* Returns the index of a node within the list.
*
* @param {Array} list
* The list of nodes.
* @param {Node} node
* The node to find.
* @returns {number}
* The index of the node within the list, or -1 if not found.
* @private
*/
function _findNodeIndex(list, node) {
var i = 0;
while (i < list.length) {
if (list[i] === node) {
return i;
}
i++;
}
}
/**
* Adds a new attribute to the list and updates the owner element of the attribute.
*
* @param {Element} el
* The element which will become the owner of the new attribute.
* @param {NamedNodeMap} list
* The list to which the new attribute will be added.
* @param {Attr} newAttr
* The new attribute to be added.
* @param {Attr} oldAttr
* The old attribute to be replaced, or null if no attribute is to be replaced.
* @returns {void}
* @private
*/
function _addNamedNode(el, list, newAttr, oldAttr) {
if (oldAttr) {
list[_findNodeIndex(list, oldAttr)] = newAttr;
} else {
list[list.length] = newAttr;
list.length++;
}
if (el) {
newAttr.ownerElement = el;
var doc = el.ownerDocument;
if (doc) {
oldAttr && _onRemoveAttribute(doc, el, oldAttr);
_onAddAttribute(doc, el, newAttr);
}
}
}
/**
* Removes an attribute from the list and updates the owner element of the attribute.
*
* @param {Element} el
* The element which is the current owner of the attribute.
* @param {NamedNodeMap} list
* The list from which the attribute will be removed.
* @param {Attr} attr
* The attribute to be removed.
* @returns {void}
* @private
*/
function _removeNamedNode(el, list, attr) {
//console.log('remove attr:'+attr)
var i = _findNodeIndex(list, attr);
if (i >= 0) {
var lastIndex = list.length - 1;
while (i <= lastIndex) {
list[i] = list[++i];
}
list.length = lastIndex;
if (el) {
var doc = el.ownerDocument;
if (doc) {
_onRemoveAttribute(doc, el, attr);
}
attr.ownerElement = null;
}
}
}
NamedNodeMap.prototype = {
length: 0,
item: NodeList.prototype.item,
/**
* Get an attribute by name. Note: Name is in lower case in case of HTML namespace and
* document.
*
* @param {string} localName
* The local name of the attribute.
* @returns {Attr | null}
* The attribute with the given local name, or null if no such attribute exists.
* @see https://dom.spec.whatwg.org/#concept-element-attributes-get-by-name
*/
getNamedItem: function (localName) {
if (this._ownerElement && this._ownerElement._isInHTMLDocumentAndNamespace()) {
localName = localName.toLowerCase();
}
var i = 0;
while (i < this.length) {
var attr = this[i];
if (attr.nodeName === localName) {
return attr;
}
i++;
}
return null;
},
/**
* Set an attribute.
*
* @param {Attr} attr
* The attribute to set.
* @returns {Attr | null}
* The old attribute with the same local name and namespace URI as the new one, or null if no
* such attribute exists.
* @throws {DOMException}
* With code:
* - {@link INUSE_ATTRIBUTE_ERR} - If the attribute is already an attribute of another
* element.
* @see https://dom.spec.whatwg.org/#concept-element-attributes-set
*/
setNamedItem: function (attr) {
var el = attr.ownerElement;
if (el && el !== this._ownerElement) {
throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR);
}
var oldAttr = this.getNamedItemNS(attr.namespaceURI, attr.localName);
if (oldAttr === attr) {
return attr;
}
_addNamedNode(this._ownerElement, this, attr, oldAttr);
return oldAttr;
},
/**
* Set an attribute, replacing an existing attribute with the same local name and namespace
* URI if one exists.
*
* @param {Attr} attr
* The attribute to set.
* @returns {Attr | null}
* The old attribute with the same local name and namespace URI as the new one, or null if no
* such attribute exists.
* @throws {DOMException}
* Throws a DOMException with the name "InUseAttributeError" if the attribute is already an
* attribute of another element.
* @see https://dom.spec.whatwg.org/#concept-element-attributes-set
*/
setNamedItemNS: function (attr) {
return this.setNamedItem(attr);
},
/**
* Removes an attribute specified by the local name.
*
* @param {string} localName
* The local name of the attribute to be removed.
* @returns {Attr}
* The attribute node that was removed.
* @throws {DOMException}
* With code:
* - {@link DOMException.NOT_FOUND_ERR} if no attribute with the given name is found.
* @see https://dom.spec.whatwg.org/#dom-namednodemap-removenameditem
* @see https://dom.spec.whatwg.org/#concept-element-attributes-remove-by-name
*/
removeNamedItem: function (localName) {
var attr = this.getNamedItem(localName);
if (!attr) {
throw new DOMException(DOMException.NOT_FOUND_ERR, localName);
}
_removeNamedNode(this._ownerElement, this, attr);
return attr;
},
/**
* Removes an attribute specified by the namespace and local name.
*
* @param {string | null} namespaceURI
* The namespace URI of the attribute to be removed.
* @param {string} localName
* The local name of the attribute to be removed.
* @returns {Attr}
* The attribute node that was removed.
* @throws {DOMException}
* With code:
* - {@link DOMException.NOT_FOUND_ERR} if no attribute with the given namespace URI and local
* name is found.
* @see https://dom.spec.whatwg.org/#dom-namednodemap-removenameditemns
* @see https://dom.spec.whatwg.org/#concept-element-attributes-remove-by-namespace
*/
removeNamedItemNS: function (namespaceURI, localName) {
var attr = this.getNamedItemNS(namespaceURI, localName);
if (!attr) {
throw new DOMException(DOMException.NOT_FOUND_ERR, namespaceURI ? namespaceURI + ' : ' + localName : localName);
}
_removeNamedNode(this._ownerElement, this, attr);
return attr;
},
/**
* Get an attribute by namespace and local name.
*
* @param {string | null} namespaceURI
* The namespace URI of the attribute.
* @param {string} localName
* The local name of the attribute.
* @returns {Attr | null}
* The attribute with the given namespace URI and local name, or null if no such attribute
* exists.
* @see https://dom.spec.whatwg.org/#concept-element-attributes-get-by-namespace
*/
getNamedItemNS: function (namespaceURI, localName) {
if (!namespaceURI) {
namespaceURI = null;
}
var i = 0;
while (i < this.length) {
var node = this[i];
if (node.localName === localName && node.namespaceURI === namespaceURI) {
return node;
}
i++;
}
return null;
},
};
NamedNodeMap.prototype[Symbol.iterator] = function () {
var me = this;
var index = 0;
return {
next: function () {
if (index < me.length) {
return {
value: me[index++],
done: false,
};
} else {
return {
done: true,
};
}
},
return: function () {
return {
done: true,
};
},
};
};
/**
* The DOMImplementation interface provides a number of methods for performing operations that
* are independent of any particular instance of the document object model.
*
* The DOMImplementation interface represents an object providing methods which are not
* dependent on any particular document.
* Such an object is returned by the `Document.implementation` property.
*
* **The individual methods describe the differences compared to the specs**.
*
* @class DOMImplementation
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation MDN
* @see https://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-102161490 DOM Level 1 Core
* (Initial)
* @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-102161490 DOM Level 2 Core
* @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-102161490 DOM Level 3 Core
* @see https://dom.spec.whatwg.org/#domimplementation DOM Living Standard
* @constructs DOMImplementation
*/
function DOMImplementation() {}
DOMImplementation.prototype = {
/**
* Test if the DOM implementation implements a specific feature and version, as specified in
* {@link https://www.w3.org/TR/DOM-Level-3-Core/core.html#DOMFeatures DOM Features}.
*
* The DOMImplementation.hasFeature() method returns a Boolean flag indicating if a given
* feature is supported. The different implementations fairly diverged in what kind of
* features were reported. The latest version of the spec settled to force this method to
* always return true, where the functionality was accurate and in use.
*
* @deprecated
* It is deprecated and modern browsers return true in all cases.
* @function DOMImplementation#hasFeature
* @param {string} feature
* The name of the feature to test.
* @param {string} [version]
* This is the version number of the feature to test.
* @returns {boolean}
* Always returns true.
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/hasFeature MDN
* @see https://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-5CED94D7 DOM Level 1 Core
* @see https://dom.spec.whatwg.org/#dom-domimplementation-hasfeature DOM Living Standard
* @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-5CED94D7 DOM Level 3 Core
*/
hasFeature: function (feature, version) {
return true;
},
/**
* Creates a DOM Document object of the specified type with its document element. Note that
* based on the {@link DocumentType}
* given to create the document, the implementation may instantiate specialized
* {@link Document} objects that support additional features than the "Core", such as "HTML"
* {@link https://www.w3.org/TR/DOM-Level-3-Core/references.html#DOM2HTML DOM Level 2 HTML}.
* On the other hand, setting the {@link DocumentType} after the document was created makes
* this very unlikely to happen. Alternatively, specialized {@link Document} creation methods,
* such as createHTMLDocument
* {@link https://www.w3.org/TR/DOM-Level-3-Core/references.html#DOM2HTML DOM Level 2 HTML},
* can be used to obtain specific types of {@link Document} objects.
*
* __It behaves slightly different from the description in the living standard__:
* - There is no interface/class `XMLDocument`, it returns a `Document`
* instance (with it's `type` set to `'xml'`).
* - `encoding`, `mode`, `origin`, `url` fields are currently not declared.
*
* @function DOMImplementation.createDocument
* @param {string | null} namespaceURI
* The
* {@link https://www.w3.org/TR/DOM-Level-3-Core/glossary.html#dt-namespaceURI namespace URI}
* of the document element to create or null.
* @param {string | null} qualifiedName
* The
* {@link https://www.w3.org/TR/DOM-Level-3-Core/glossary.html#dt-qualifiedname qualified name}
* of the document element to be created or null.
* @param {DocumentType | null} [doctype=null]
* The type of document to be created or null. When doctype is not null, its
* {@link Node#ownerDocument} attribute is set to the document being created. Default is
* `null`
* @returns {Document}
* A new {@link Document} object with its document element. If the NamespaceURI,
* qualifiedName, and doctype are null, the returned {@link Document} is empty with no
* document element.
* @throws {DOMException}
* With code:
*
* - `INVALID_CHARACTER_ERR`: Raised if the specified qualified name is not an XML name
* according to {@link https://www.w3.org/TR/DOM-Level-3-Core/references.html#XML XML 1.0}.
* - `NAMESPACE_ERR`: Raised if the qualifiedName is malformed, if the qualifiedName has a
* prefix and the namespaceURI is null, or if the qualifiedName is null and the namespaceURI
* is different from null, or if the qualifiedName has a prefix that is "xml" and the
* namespaceURI is different from "{@link http://www.w3.org/XML/1998/namespace}"
* {@link https://www.w3.org/TR/DOM-Level-3-Core/references.html#Namespaces XML Namespaces},
* or if the DOM implementation does not support the "XML" feature but a non-null namespace
* URI was provided, since namespaces were defined by XML.
* - `WRONG_DOCUMENT_ERR`: Raised if doctype has already been used with a different document
* or was created from a different implementation.
* - `NOT_SUPPORTED_ERR`: May be raised if the implementation does not support the feature
* "XML" and the language exposed through the Document does not support XML Namespaces (such
* as {@link https://www.w3.org/TR/DOM-Level-3-Core/references.html#HTML40 HTML 4.01}).
* @since DOM Level 2.
* @see {@link #createHTMLDocument}
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/createDocument MDN
* @see https://dom.spec.whatwg.org/#dom-domimplementation-createdocument DOM Living Standard
* @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#Level-2-Core-DOM-createDocument DOM
* Level 3 Core
* @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#Level-2-Core-DOM-createDocument DOM
* Level 2 Core (initial)
*/
createDocument: function (namespaceURI, qualifiedName, doctype) {
var contentType = MIME_TYPE.XML_APPLICATION;
if (namespaceURI === NAMESPACE.HTML) {
contentType = MIME_TYPE.XML_XHTML_APPLICATION;
} else if (namespaceURI === NAMESPACE.SVG) {
contentType = MIME_TYPE.XML_SVG_IMAGE;
}
var doc = new Document(PDC, { contentType: contentType });
doc.implementation = this;
doc.childNodes = new NodeList();
doc.doctype = doctype || null;
if (doctype) {
doc.appendChild(doctype);
}
if (qualifiedName) {
var root = doc.createElementNS(namespaceURI, qualifiedName);
doc.appendChild(root);
}
return doc;
},
/**
* Creates an empty DocumentType node. Entity declarations and notations are not made
* available. Entity reference expansions and default attribute additions do not occur.
*
* **This behavior is slightly different from the one in the specs**:
* - `encoding`, `mode`, `origin`, `url` fields are currently not declared.
* - `publicId` and `systemId` contain the raw data including any possible quotes,
* so they can always be serialized back to the original value
* - `internalSubset` contains the raw string between `[` and `]` if present,
* but is not parsed or validated in any form.
*
* @function DOMImplementation#createDocumentType
* @param {string} qualifiedName
* The {@link https://www.w3.org/TR/DOM-Level-3-Core/glossary.html#dt-qualifiedname qualified
* name} of the document type to be created.
* @param {string} [publicId]
* The external subset public identifier. Stored verbatim including surrounding quotes.
* When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
* if the value is non-empty and does not match the XML `PubidLiteral` production
* (W3C DOM Parsing §3.2.1.3; XML 1.0 production [12]). Creation-time validation is not
* enforced — deferred to a future breaking release.
* @param {string} [systemId]
* The external subset system identifier. Stored verbatim including surrounding quotes.
* When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
* if the value is non-empty and does not match the XML `SystemLiteral` production
* (W3C DOM Parsing §3.2.1.3; XML 1.0 production [11]). Creation-time validation is not
* enforced — deferred to a future breaking release.
* @param {string} [internalSubset]
* The internal subset or an empty string if it is not present. Stored verbatim.
* When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
* if the value contains `"]>"`. Creation-time validation is not enforced.
* @returns {DocumentType}
* A new {@link DocumentType} node with {@link Node#ownerDocument} set to null.
* @throws {DOMException}
* With code:
*
* - `INVALID_CHARACTER_ERR`: Raised if the specified qualified name is not an XML name
* according to {@link https://www.w3.org/TR/DOM-Level-3-Core/references.html#XML XML 1.0}.
* - `NAMESPACE_ERR`: Raised if the qualifiedName is malformed.
* - `NOT_SUPPORTED_ERR`: May be raised if the implementation does not support the feature
* "XML" and the language exposed through the Document does not support XML Namespaces (such
* as {@link https://www.w3.org/TR/DOM-Level-3-Core/references.html#HTML40 HTML 4.01}).
* @since DOM Level 2.
* @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/createDocumentType
* MDN
* @see https://dom.spec.whatwg.org/#dom-domimplementation-createdocumenttype DOM Living
* Standard
* @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#Level-3-Core-DOM-createDocType DOM
* Level 3 Core
* @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#Level-2-Core-DOM-createDocType DOM
* Level 2 Core
* @see https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md#050
* @see https://www.w3.org/TR/DOM-Level-2-Core/#core-ID-Core-DocType-internalSubset
* @prettierignore
*/
createDocumentType: function (qualifiedName, publicId, systemId, internalSubset) {
validateQualifiedName(qualifiedName);
var node = new DocumentType(PDC);
node.name = qualifiedName;
node.nodeName = qualifiedName;
node.publicId = publicId || '';
node.systemId = systemId || '';
node.internalSubset = internalSubset || '';
node.childNodes = new NodeList();
return node;
},
/**
* Returns an HTML document, that might already have a basic DOM structure.
*
* __It behaves slightly different from the description in the living standard__:
* - If the first argument is `false` no initial nodes are added (steps 3-7 in the specs are
* omitted)
* - `encoding`, `mode`, `origin`, `url` fields are currently not declared.
*
* @param {string | false} [title]
* A string containing the title to give the new HTML document.
* @returns {Document}
* The HTML document.
* @since WHATWG Living Standard.
* @see {@link #createDocument}
* @see https://dom.spec.whatwg.org/#dom-domimplementation-createhtmldocument
* @see https://dom.spec.whatwg.org/#html-document
*/
createHTMLDocument: function (title) {
var doc = new Document(PDC, { contentType: MIME_TYPE.HTML });
doc.implementation = this;
doc.childNodes = new NodeList();
if (title !== false) {
doc.doctype = this.createDocumentType('html');
doc.doctype.ownerDocument = doc;
doc.appendChild(doc.doctype);
var htmlNode = doc.createElement('html');
doc.appendChild(htmlNode);
var headNode = doc.createElement('head');
htmlNode.appendChild(headNode);
if (typeof title === 'string') {
var titleNode = doc.createElement('title');
titleNode.appendChild(doc.createTextNode(title));
headNode.appendChild(titleNode);
}
htmlNode.appendChild(doc.createElement('body'));
}
return doc;
},
};
/**
* The DOM Node interface is an abstract base class upon which many other DOM API objects are
* based, thus letting those object types to be used similarly and often interchangeably. As an
* abstract class, there is no such thing as a plain Node object. All objects that implement
* Node functionality are based on one of its subclasses. Most notable are Document, Element,
* and DocumentFragment.
*
* In addition, every kind of DOM node is represented by an interface based on Node. These
* include Attr, CharacterData (which Text, Comment, CDATASection and ProcessingInstruction are
* all based on), and DocumentType.
*
* In some cases, a particular feature of the base Node interface may not apply to one of its
* child interfaces; in that case, the inheriting node may return null or throw an exception,
* depending on circumstances. For example, attempting to add children to a node type that
* cannot have children will throw an exception.
*
* **This behavior is slightly different from the in the specs**:
* - unimplemented interfaces: `EventTarget`
*
* @class
* @abstract
* @param {Symbol} symbol
* @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247
* @see https://dom.spec.whatwg.org/#node
* @prettierignore
*/
function Node(symbol) {
checkSymbol(symbol);
}
Node.prototype = {
/**
* The first child of this node.
*
* @type {Node | null}
*/
firstChild: null,
/**
* The last child of this node.
*
* @type {Node | null}
*/
lastChild: null,
/**
* The previous sibling of this node.
*
* @type {Node | null}
*/
previousSibling: null,
/**
* The next sibling of this node.
*
* @type {Node | null}
*/
nextSibling: null,
/**
* The parent node of this node.
*
* @type {Node | null}
*/
parentNode: null,
/**
* The parent element of this node.
*
* @type {Element | null}
*/
get parentElement() {
return this.parentNode && this.parentNode.nodeType === this.ELEMENT_NODE ? this.parentNode : null;
},
/**
* The child nodes of this node.
*
* @type {NodeList}
*/
childNodes: null,
/**
* The document object associated with this node.
*
* @type {Document | null}
*/
ownerDocument: null,
/**
* The value of this node.
*
* @type {string | null}
*/
nodeValue: null,
/**
* The namespace URI of this node.
*
* @type {string | null}
*/
namespaceURI: null,
/**
* The prefix of the namespace for this node.
*
* @type {string | null}
*/
prefix: null,
/**
* The local part of the qualified name of this node.
*
* @type {string | null}
*/
localName: null,
/**
* The baseURI is currently always `about:blank`,
* since that's what happens when you create a document from scratch.
*
* @type {'about:blank'}
*/
baseURI: 'about:blank',
/**
* Is true if this node is part of a document.
*
* @type {boolean}
*/
get isConnected() {
var rootNode = this.getRootNode();
return rootNode && rootNode.nodeType === rootNode.DOCUMENT_NODE;
},
/**
* Checks whether `other` is an inclusive descendant of this node.
*
* @param {Node | null | undefined} other
* The node to check.
* @returns {boolean}
* True if `other` is an inclusive descendant of this node; false otherwise.
* @see https://dom.spec.whatwg.org/#dom-node-contains
*/
contains: function (other) {
if (!other) return false;
var parent = other;
do {
if (this === parent) return true;
parent = parent.parentNode;
} while (parent);
return false;
},
/**
* @typedef GetRootNodeOptions
* @property {boolean} [composed=false]
*/
/**
* Searches for the root node of this node.
*
* **This behavior is slightly different from the in the specs**:
* - ignores `options.composed`, since `ShadowRoot`s are unsupported, always returns root.
*
* @param {GetRootNodeOptions} [options]
* @returns {Node}
* Root node.
* @see https://dom.spec.whatwg.org/#dom-node-getrootnode
* @see https://dom.spec.whatwg.org/#concept-shadow-including-root
*/
getRootNode: function (options) {
var parent = this;
do {
if (!parent.parentNode) {
return parent;
}
parent = parent.parentNode;
} while (parent);
},
/**
* Checks whether the given node is equal to this node.
*
* Two nodes are equal when they have the same type, defining characteristics (for the type),
* and the same childNodes. The comparison is iterative to avoid stack overflows on
* deeply-nested trees. Attribute nodes of each Element pair are also pushed onto the stack
* and compared the same way.
*
* @param {Node} [otherNode]
* @returns {boolean}
* @see https://dom.spec.whatwg.org/#concept-node-equals
* @see ../docs/walk-dom.md.
*/
isEqualNode: function (otherNode) {
if (!otherNode) return false;
// Use an explicit {node, other} pair stack to avoid call-stack overflow on deep trees.
// walkDOM cannot be used here — parallel two-tree traversal requires pairing
// corresponding nodes at each step across both trees simultaneously.
var stack = [{ node: this, other: otherNode }];
while (stack.length > 0) {
var pair = stack.pop();
var node = pair.node;
var other = pair.other;
if (node.nodeType !== other.nodeType) return false;
switch (node.nodeType) {
case node.DOCUMENT_TYPE_NODE:
if (node.name !== other.name) return false;
if (node.publicId !== other.publicId) return false;
if (node.systemId !== other.systemId) return false;
break;
case node.ELEMENT_NODE:
if (node.namespaceURI !== other.namespaceURI) return false;
if (node.prefix !== other.prefix) return false;
if (node.localName !== other.localName) return false;
if (node.attributes.length !== other.attributes.length) return false;
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes.item(i);
var otherAttr = other.getAttributeNodeNS(attr.namespaceURI, attr.localName);
if (!otherAttr) return false;
stack.push({ node: attr, other: otherAttr });
}
break;
case node.ATTRIBUTE_NODE:
if (node.namespaceURI !== other.namespaceURI) return false;
if (node.localName !== other.localName) return false;
if (node.value !== other.value) return false;
break;
case node.PROCESSING_INSTRUCTION_NODE:
if (node.target !== other.target || node.data !== other.data) return false;
break;
case node.TEXT_NODE:
case node.CDATA_SECTION_NODE:
case node.COMMENT_NODE:
if (node.data !== other.data) return false;
break;
}
if (node.childNodes.length !== other.childNodes.length) return false;
// Push children in reverse order so index 0 is processed first (LIFO).
for (var i = node.childNodes.length - 1; i >= 0; i--) {
stack.push({ node: node.childNodes[i], other: other.childNodes[i] });
}
}
return true;
},
/**
* Checks whether or not the given node is this node.
*
* @param {Node} [otherNode]
*/
isSameNode: function (otherNode) {
return this === otherNode;
},
/**
* Inserts a node before a reference node as a child of this node.
*
* @param {Node} newChild
* The new child node to be inserted.
* @param {Node | null} refChild
* The reference node before which newChild will be inserted.
* @returns {Node}
* The new child node successfully inserted.
* @throws {DOMException}
* Throws a DOMException if inserting the node would result in a DOM tree that is not
* well-formed, or if `child` is provided but is not a child of `parent`.
* See {@link _insertBefore} for more details.
* @since Modified in DOM L2
*/
insertBefore: function (newChild, refChild) {
return _insertBefore(this, newChild, refChild);
},
/**
* Replaces an old child node with a new child node within this node.
*
* @param {Node} newChild
* The new node that is to replace the old node.
* If it already exists in the DOM, it is removed from its original position.
* @param {Node} oldChild
* The existing child node to be replaced.
* @returns {Node}
* Returns the replaced child node.
* @throws {DOMException}
* Throws a DOMException if replacing the node would result in a DOM tree that is not
* well-formed, or if `oldChild` is not a child of `this`.
* This can also occur if the pre-replacement validity assertion fails.
* See {@link _insertBefore}, {@link Node.removeChild}, and
* {@link assertPreReplacementValidityInDocument} for more details.
* @see https://dom.spec.whatwg.org/#concept-node-replace
*/
replaceChild: function (newChild, oldChild) {
_insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument);
if (oldChild) {
this.removeChild(oldChild);
}
},
/**
* Removes an existing child node from this node.
*
* @param {Node} oldChild
* The child node to be removed.
* @returns {Node}
* Returns the removed child node.
* @throws {DOMException}
* Throws a DOMException if `oldChild` is not a child of `this`.
* See {@link _removeChild} for more details.
*/
removeChild: function (oldChild) {
return _removeChild(this, oldChild);
},
/**
* Appends a child node to this node.
*
* @param {Node} newChild
* The child node to be appended to this node.
* If it already exists in the DOM, it is removed from its original position.
* @returns {Node}
* Returns the appended child node.
* @throws {DOMException}
* Throws a DOMException if appending the node would result in a DOM tree that is not
* well-formed, or if `newChild` is not a valid Node.
* See {@link insertBefore} for more details.
*/
appendChild: function (newChild) {
return this.insertBefore(newChild, null);
},
/**
* Determines whether this node has any child nodes.
*
* @returns {boolean}
* Returns true if this node has any child nodes, and false otherwise.
*/
hasChildNodes: function () {
return this.firstChild != null;
},
/**
* Creates a copy of the calling node.
*
* @param {boolean} deep
* If true, the contents of the node are recursively copied.
* If false, only the node itself (and its attributes, if it is an element) are copied.
* @returns {Node}
* Returns the newly created copy of the node.
* @throws {DOMException}
* May throw a DOMException if operations within {@link Element#setAttributeNode} or
* {@link Node#appendChild} (which are potentially invoked in this method) do not meet their
* specific constraints.
* @see {@link cloneNode}
*/
cloneNode: function (deep) {
return cloneNode(this.ownerDocument || this, this, deep);
},
/**
* Puts the specified node and all of its subtree into a "normalized" form. In a normalized
* subtree, no text nodes in the subtree are empty and there are no adjacent text nodes.
*
* Specifically, this method merges any adjacent text nodes (i.e., nodes for which `nodeType`
* is `TEXT_NODE`) into a single node with the combined data. It also removes any empty text
* nodes.
*
* This method iterativly traverses all child nodes to normalize all descendent nodes within
* the subtree.
*
* @throws {DOMException}
* May throw a DOMException if operations within removeChild or appendData (which are
* potentially invoked in this method) do not meet their specific constraints.
* @since Modified in DOM Level 2
* @see {@link Node.removeChild}
* @see {@link CharacterData.appendData}
* @see ../docs/walk-dom.md.
*/
normalize: function () {
walkDOM(this, null, {
enter: function (node) {
// Merge adjacent text children of node before walkDOM schedules them.
// walkDOM reads lastChild/previousSibling after enter returns, so the
// surviving post-merge children are what it descends into.
var child = node.firstChild;
while (child) {
var next = child.nextSibling;
if (next !== null && next.nodeType === TEXT_NODE && child.nodeType === TEXT_NODE) {
node.removeChild(next);
child.appendData(next.data);
// Do not advance child: re-check new nextSibling for another text run
} else {
child = next;
}
}
return true; // descend into surviving children
},
});
},
/**
* Checks whether the DOM implementation implements a specific feature and its version.
*
* @deprecated
* Since `DOMImplementation.hasFeature` is deprecated and always returns true.
* @param {string} feature
* The package name of the feature to test. This is the same name that can be passed to the
* method `hasFeature` on `DOMImplementation`.
* @param {string} version
* This is the version number of the package name to test.
* @returns {boolean}
* Returns true in all cases in the current implementation.
* @since Introduced in DOM Level 2
* @see {@link DOMImplementation.hasFeature}
*/
isSupported: function (feature, version) {
return this.ownerDocument.implementation.hasFeature(feature, version);
},
/**
* Look up the prefix associated to the given namespace URI, starting from this node.
* **The default namespace declarations are ignored by this method.**
* See Namespace Prefix Lookup for details on the algorithm used by this method.
*
* **This behavior is different from the in the specs**:
* - no node type specific handling
* - uses the internal attribute _nsMap for resolving namespaces that is updated when changing attributes
*
* @param {string | null} namespaceURI
* The namespace URI for which to find the associated prefix.
* @returns {string | null}
* The associated prefix, if found; otherwise, null.
* @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-lookupNamespacePrefix
* @see https://www.w3.org/TR/DOM-Level-3-Core/namespaces-algorithms.html#lookupNamespacePrefixAlgo
* @see https://dom.spec.whatwg.org/#dom-node-lookupprefix
* @see https://github.com/xmldom/xmldom/issues/322
* @prettierignore
*/
lookupPrefix: function (namespaceURI) {
var el = this;
while (el) {
var map = el._nsMap;
//console.dir(map)
if (map) {
for (var n in map) {
if (hasOwn(map, n) && map[n] === namespaceURI) {
return n;
}
}
}
el = el.nodeType == ATTRIBUTE_NODE ? el.ownerDocument : el.parentNode;
}
return null;
},
/**
* This function is used to look up the namespace URI associated with the given prefix,
* starting from this node.
*
* **This behavior is different from the in the specs**:
* - no node type specific handling
* - uses the internal attribute _nsMap for resolving namespaces that is updated when changing attributes
*
* @param {string | null} prefix
* The prefix for which to find the associated namespace URI.
* @returns {string | null}
* The associated namespace URI, if found; otherwise, null.
* @since DOM Level 3
* @see https://dom.spec.whatwg.org/#dom-node-lookupnamespaceuri
* @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-lookupNamespaceURI
* @prettierignore
*/
lookupNamespaceURI: function (prefix) {
var el = this;
while (el) {
var map = el._nsMap;
//console.dir(map)
if (map) {
if (hasOwn(map, prefix)) {
return map[prefix];
}
}
el = el.nodeType == ATTRIBUTE_NODE ? el.ownerDocument : el.parentNode;
}
return null;
},
/**
* Determines whether the given namespace URI is the default namespace.
*
* The function works by looking up the prefix associated with the given namespace URI. If no
* prefix is found (i.e., the namespace URI is not registered in the namespace map of this
* node or any of its ancestors), it returns `true`, implying the namespace URI is considered
* the default.
*
* **This behavior is dif