blob: 94f8d484b2d21a93dffa5d0102ffab16d1c1ae2b [file] [log] [blame]
"use strict";
const { CookieJar } = require("tough-cookie");
const NodeImpl = require("./Node-impl").implementation;
const idlUtils = require("../generated/utils");
const NODE_TYPE = require("../node-type");
const { mixin, memoizeQuery } = require("../../utils");
const { firstChildWithLocalName, firstChildWithLocalNames, firstDescendantWithLocalName } =
require("../helpers/traversal");
const whatwgURL = require("whatwg-url");
const StyleSheetList = require("../generated/StyleSheetList.js");
const { domSymbolTree } = require("../helpers/internal-constants");
const eventAccessors = require("../helpers/create-event-accessor");
const { asciiLowercase, stripAndCollapseASCIIWhitespace } = require("../helpers/strings");
const { childTextContent } = require("../helpers/text");
const { HTML_NS, SVG_NS } = require("../helpers/namespaces");
const DOMException = require("domexception/webidl2js-wrapper");
const { parseIntoDocument } = require("../../browser/parser");
const History = require("../generated/History");
const Location = require("../generated/Location");
const HTMLCollection = require("../generated/HTMLCollection");
const NodeList = require("../generated/NodeList");
const validateName = require("../helpers/validate-names").name;
const { validateAndExtract } = require("../helpers/validate-names");
const { fireAnEvent } = require("../helpers/events");
const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shadow-dom");
const { enqueueCECallbackReaction } = require("../helpers/custom-elements");
const { createElement, internalCreateElementNSSteps } = require("../helpers/create-element");
const DocumentOrShadowRootImpl = require("./DocumentOrShadowRoot-impl").implementation;
const GlobalEventHandlersImpl = require("./GlobalEventHandlers-impl").implementation;
const NonElementParentNodeImpl = require("./NonElementParentNode-impl").implementation;
const ParentNodeImpl = require("./ParentNode-impl").implementation;
const { clone, listOfElementsWithQualifiedName, listOfElementsWithNamespaceAndLocalName,
listOfElementsWithClassNames } = require("../node");
const generatedAttr = require("../generated/Attr");
const Comment = require("../generated/Comment");
const ProcessingInstruction = require("../generated/ProcessingInstruction");
const CDATASection = require("../generated/CDATASection");
const Text = require("../generated/Text");
const DocumentFragment = require("../generated/DocumentFragment");
const DOMImplementation = require("../generated/DOMImplementation");
const TreeWalker = require("../generated/TreeWalker");
const NodeIterator = require("../generated/NodeIterator");
const ShadowRoot = require("../generated/ShadowRoot");
const Range = require("../generated/Range");
const documents = require("../documents.js");
const CustomEvent = require("../generated/CustomEvent");
const ErrorEvent = require("../generated/ErrorEvent");
const Event = require("../generated/Event");
const FocusEvent = require("../generated/FocusEvent");
const HashChangeEvent = require("../generated/HashChangeEvent");
const KeyboardEvent = require("../generated/KeyboardEvent");
const MessageEvent = require("../generated/MessageEvent");
const MouseEvent = require("../generated/MouseEvent");
const PopStateEvent = require("../generated/PopStateEvent");
const ProgressEvent = require("../generated/ProgressEvent");
const TouchEvent = require("../generated/TouchEvent");
const UIEvent = require("../generated/UIEvent");
const RequestManager = require("../../browser/resources/request-manager");
const AsyncResourceQueue = require("../../browser/resources/async-resource-queue");
const ResourceQueue = require("../../browser/resources/resource-queue");
const PerDocumentResourceLoader = require("../../browser/resources/per-document-resource-loader");
function clearChildNodes(node) {
for (let child = domSymbolTree.firstChild(node); child; child = domSymbolTree.firstChild(node)) {
node.removeChild(child);
}
}
function pad(number) {
if (number < 10) {
return "0" + number;
}
return number;
}
function toLastModifiedString(date) {
return pad(date.getMonth() + 1) +
"/" + pad(date.getDate()) +
"/" + date.getFullYear() +
" " + pad(date.getHours()) +
":" + pad(date.getMinutes()) +
":" + pad(date.getSeconds());
}
const eventInterfaceTable = {
customevent: CustomEvent,
errorevent: ErrorEvent,
event: Event,
events: Event,
focusevent: FocusEvent,
hashchangeevent: HashChangeEvent,
htmlevents: Event,
keyboardevent: KeyboardEvent,
messageevent: MessageEvent,
mouseevent: MouseEvent,
mouseevents: MouseEvent,
popstateevent: PopStateEvent,
progressevent: ProgressEvent,
svgevents: Event,
touchevent: TouchEvent,
uievent: UIEvent,
uievents: UIEvent
};
class DocumentImpl extends NodeImpl {
constructor(globalObject, args, privateData) {
super(globalObject, args, privateData);
this._initGlobalEvents();
this._ownerDocument = this;
this.nodeType = NODE_TYPE.DOCUMENT_NODE;
if (!privateData.options) {
privateData.options = {};
}
if (!privateData.options.parsingMode) {
privateData.options.parsingMode = "xml";
}
if (!privateData.options.encoding) {
privateData.options.encoding = "UTF-8";
}
if (!privateData.options.contentType) {
privateData.options.contentType = privateData.options.parsingMode === "xml" ? "application/xml" : "text/html";
}
this._parsingMode = privateData.options.parsingMode;
this._implementation = DOMImplementation.createImpl(this._globalObject, [], {
ownerDocument: this
});
this._defaultView = privateData.options.defaultView || null;
this._global = privateData.options.global;
this._ids = Object.create(null);
this._attached = true;
this._currentScript = null;
this._pageShowingFlag = false;
this._cookieJar = privateData.options.cookieJar;
this._parseOptions = privateData.options.parseOptions;
this._scriptingDisabled = privateData.options.scriptingDisabled;
if (this._cookieJar === undefined) {
this._cookieJar = new CookieJar(null, { looseMode: true });
}
this.contentType = privateData.options.contentType;
this._encoding = privateData.options.encoding;
const urlOption = privateData.options.url === undefined ? "about:blank" : privateData.options.url;
const parsed = whatwgURL.parseURL(urlOption);
if (parsed === null) {
throw new TypeError(`Could not parse "${urlOption}" as a URL`);
}
this._URL = parsed;
this._origin = whatwgURL.serializeURLOrigin(parsed);
this._location = Location.createImpl(this._globalObject, [], { relevantDocument: this });
this._history = History.createImpl(this._globalObject, [], {
window: this._defaultView,
document: this,
actAsIfLocationReloadCalled: () => this._location.reload()
});
this._workingNodeIterators = [];
this._workingNodeIteratorsMax = privateData.options.concurrentNodeIterators === undefined ?
10 :
Number(privateData.options.concurrentNodeIterators);
if (isNaN(this._workingNodeIteratorsMax)) {
throw new TypeError("The 'concurrentNodeIterators' option must be a Number");
}
if (this._workingNodeIteratorsMax < 0) {
throw new RangeError("The 'concurrentNodeIterators' option must be a non negative Number");
}
this._referrer = privateData.options.referrer || "";
this._lastModified = toLastModifiedString(privateData.options.lastModified || new Date());
this._asyncQueue = new AsyncResourceQueue();
this._queue = new ResourceQueue({ asyncQueue: this._asyncQueue, paused: false });
this._deferQueue = new ResourceQueue({ paused: true });
this._requestManager = new RequestManager();
this._currentDocumentReadiness = privateData.options.readyState || "loading";
this._lastFocusedElement = null;
this._resourceLoader = new PerDocumentResourceLoader(this);
// Each Document in a browsing context can also have a latest entry. This is the entry for that Document
// to which the browsing context's session history was most recently traversed. When a Document is created,
// it initially has no latest entry.
this._latestEntry = null;
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter
this._throwOnDynamicMarkupInsertionCounter = 0;
}
_getTheParent(event) {
if (event.type === "load" || !this._defaultView) {
return null;
}
return idlUtils.implForWrapper(this._defaultView);
}
get compatMode() {
return this._parsingMode === "xml" || this.doctype ? "CSS1Compat" : "BackCompat";
}
get charset() {
return this._encoding;
}
get characterSet() {
return this._encoding;
}
get inputEncoding() {
return this._encoding;
}
get doctype() {
for (const childNode of domSymbolTree.childrenIterator(this)) {
if (childNode.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) {
return childNode;
}
}
return null;
}
get URL() {
return whatwgURL.serializeURL(this._URL);
}
get documentURI() {
return whatwgURL.serializeURL(this._URL);
}
get location() {
return this._defaultView ? this._location : null;
}
// https://dom.spec.whatwg.org/#dom-document-documentelement
get documentElement() {
for (const childNode of domSymbolTree.childrenIterator(this)) {
if (childNode.nodeType === NODE_TYPE.ELEMENT_NODE) {
return childNode;
}
}
return null;
}
get implementation() {
return this._implementation;
}
set implementation(implementation) {
this._implementation = implementation;
}
get defaultView() {
return this._defaultView;
}
get currentScript() {
return this._currentScript;
}
get readyState() {
return this._currentDocumentReadiness;
}
set readyState(state) {
this._currentDocumentReadiness = state;
fireAnEvent("readystatechange", this);
}
hasFocus() {
return Boolean(this._lastFocusedElement);
}
_descendantRemoved(parent, child) {
if (child.tagName === "STYLE") {
this.styleSheets._remove(child.sheet);
}
super._descendantRemoved.apply(this, arguments);
}
write() {
let text = "";
for (let i = 0; i < arguments.length; ++i) {
text += String(arguments[i]);
}
if (this._parsingMode === "xml") {
throw DOMException.create(this._globalObject, [
"Cannot use document.write on XML documents",
"InvalidStateError"
]);
}
if (this._throwOnDynamicMarkupInsertionCounter > 0) {
throw DOMException.create(this._globalObject, [
"Cannot use document.write while a custom element upgrades",
"InvalidStateError"
]);
}
if (this._writeAfterElement) {
// If called from an script element directly (during the first tick),
// the new elements are inserted right after that element.
const tempDiv = this.createElement("div");
tempDiv.innerHTML = text;
let child = tempDiv.firstChild;
let previous = this._writeAfterElement;
const parent = this._writeAfterElement.parentNode;
while (child) {
const node = child;
child = child.nextSibling;
node._isMovingDueToDocumentWrite = true; // hack for script execution
parent.insertBefore(node, previous.nextSibling);
node._isMovingDueToDocumentWrite = false;
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.
if (this.lastChild) {
let node = this;
while (node.lastChild && node.lastChild.nodeType === NODE_TYPE.ELEMENT_NODE) {
node = node.lastChild;
}
node.innerHTML = text;
} else {
clearChildNodes(this);
parseIntoDocument(text, this);
}
} else if (text) {
clearChildNodes(this);
parseIntoDocument(text, this);
}
}
writeln() {
this.write(...arguments, "\n");
}
// This is implemented separately for Document (which has a _ids cache) and DocumentFragment (which does not).
getElementById(id) {
if (!this._ids[id]) {
return null;
}
// Let's find the first element with where it's root is the document.
const matchElement = this._ids[id].find(candidate => {
let root = candidate;
while (domSymbolTree.parent(root)) {
root = domSymbolTree.parent(root);
}
return root === this;
});
return matchElement || null;
}
get referrer() {
return this._referrer || "";
}
get lastModified() {
return this._lastModified;
}
get images() {
return this.getElementsByTagName("IMG");
}
get embeds() {
return this.getElementsByTagName("EMBED");
}
get plugins() {
return this.embeds;
}
get links() {
return HTMLCollection.createImpl(this._globalObject, [], {
element: this,
query: () => domSymbolTree.treeToArray(this, {
filter: node => (node._localName === "a" || node._localName === "area") &&
node.hasAttributeNS(null, "href") &&
node._namespaceURI === HTML_NS
})
});
}
get forms() {
return this.getElementsByTagName("FORM");
}
get scripts() {
return this.getElementsByTagName("SCRIPT");
}
get anchors() {
return HTMLCollection.createImpl(this._globalObject, [], {
element: this,
query: () => domSymbolTree.treeToArray(this, {
filter: node => node._localName === "a" &&
node.hasAttributeNS(null, "name") &&
node._namespaceURI === HTML_NS
})
});
}
// The applets attribute must return an
// HTMLCollection rooted at the Document node,
// whose filter matches nothing.
// (It exists for historical reasons.)
get applets() {
return HTMLCollection.createImpl(this._globalObject, [], {
element: this,
query: () => []
});
}
open() {
let child = domSymbolTree.firstChild(this);
while (child) {
this.removeChild(child);
child = domSymbolTree.firstChild(this);
}
this._modified();
return this;
}
close(noQueue) {
// In some cases like when creating an empty iframe, I want to emit the
// events right away to avoid problems if later I asign the property src.
if (noQueue) {
this.readyState = "complete";
fireAnEvent("DOMContentLoaded", this, undefined, { bubbles: true });
fireAnEvent("load", this);
return;
}
this._queue.resume();
const dummyPromise = Promise.resolve();
const onDOMContentLoad = () => {
const doc = this;
function dispatchEvent() {
// https://html.spec.whatwg.org/#the-end
doc.readyState = "interactive";
fireAnEvent("DOMContentLoaded", doc, undefined, { bubbles: true });
}
return new Promise(resolve => {
if (!this._deferQueue.tail) {
dispatchEvent();
return resolve();
}
this._deferQueue.setListener(() => {
dispatchEvent();
resolve();
});
return this._deferQueue.resume();
});
};
const onLoad = () => {
const doc = this;
function dispatchEvent() {
doc.readyState = "complete";
fireAnEvent("load", doc);
}
return new Promise(resolve => {
if (this._asyncQueue.count() === 0) {
dispatchEvent();
return resolve();
}
return this._asyncQueue.setListener(() => {
dispatchEvent();
resolve();
});
});
};
this._queue.push(dummyPromise, onDOMContentLoad, null);
// Set the readyState to 'complete' once all resources are loaded.
// As a side-effect the document's load-event will be dispatched.
this._queue.push(dummyPromise, onLoad, null, true);
}
getElementsByName(elementName) {
return NodeList.createImpl(this._globalObject, [], {
element: this,
query: () => domSymbolTree.treeToArray(this, {
filter: node => node.getAttributeNS && node.getAttributeNS(null, "name") === elementName
})
});
}
get title() {
const { documentElement } = this;
let value = "";
if (documentElement && documentElement._localName === "svg") {
const svgTitleElement = firstChildWithLocalName(documentElement, "title", SVG_NS);
if (svgTitleElement) {
value = childTextContent(svgTitleElement);
}
} else {
const titleElement = firstDescendantWithLocalName(this, "title");
if (titleElement) {
value = childTextContent(titleElement);
}
}
value = stripAndCollapseASCIIWhitespace(value);
return value;
}
set title(value) {
const { documentElement } = this;
let element;
if (documentElement && documentElement._localName === "svg") {
element = firstChildWithLocalName(documentElement, "title", SVG_NS);
if (!element) {
element = this.createElementNS(SVG_NS, "title");
this._insert(element, documentElement.firstChild);
}
element.textContent = value;
} else if (documentElement && documentElement._namespaceURI === HTML_NS) {
const titleElement = firstDescendantWithLocalName(this, "title");
const headElement = this.head;
if (titleElement === null && headElement === null) {
return;
}
if (titleElement !== null) {
element = titleElement;
} else {
element = this.createElement("title");
headElement._append(element);
}
element.textContent = value;
}
}
get dir() {
return this.documentElement ? this.documentElement.dir : "";
}
set dir(value) {
if (this.documentElement) {
this.documentElement.dir = value;
}
}
get head() {
return this.documentElement ? firstChildWithLocalName(this.documentElement, "head") : null;
}
get body() {
const { documentElement } = this;
if (!documentElement || documentElement._localName !== "html" ||
documentElement._namespaceURI !== HTML_NS) {
return null;
}
return firstChildWithLocalNames(this.documentElement, new Set(["body", "frameset"]));
}
set body(value) {
if (value === null ||
value._namespaceURI !== HTML_NS ||
(value._localName !== "body" && value._localName !== "frameset")) {
throw DOMException.create(this._globalObject, [
"Cannot set the body to null or a non-body/frameset element",
"HierarchyRequestError"
]);
}
const bodyElement = this.body;
if (value === bodyElement) {
return;
}
if (bodyElement !== null) {
bodyElement.parentNode._replace(value, bodyElement);
return;
}
const { documentElement } = this;
if (documentElement === null) {
throw DOMException.create(this._globalObject, [
"Cannot set the body when there is no document element",
"HierarchyRequestError"
]);
}
documentElement._append(value);
}
_runPreRemovingSteps(oldNode) {
for (const activeNodeIterator of this._workingNodeIterators) {
activeNodeIterator._preRemovingSteps(oldNode);
}
}
createEvent(type) {
const typeLower = type.toLowerCase();
const eventWrapper = eventInterfaceTable[typeLower] || null;
if (!eventWrapper) {
throw DOMException.create(this._globalObject, [
"The provided event type (\"" + type + "\") is invalid",
"NotSupportedError"
]);
}
const impl = eventWrapper.createImpl(this._globalObject, [""]);
impl._initializedFlag = false;
return impl;
}
createRange() {
return Range.createImpl(this._globalObject, [], {
start: { node: this, offset: 0 },
end: { node: this, offset: 0 }
});
}
createProcessingInstruction(target, data) {
validateName(this._globalObject, target);
if (data.includes("?>")) {
throw DOMException.create(this._globalObject, [
"Processing instruction data cannot contain the string \"?>\"",
"InvalidCharacterError"
]);
}
return ProcessingInstruction.createImpl(this._globalObject, [], {
ownerDocument: this,
target,
data
});
}
// https://dom.spec.whatwg.org/#dom-document-createcdatasection
createCDATASection(data) {
if (this._parsingMode === "html") {
throw DOMException.create(this._globalObject, [
"Cannot create CDATA sections in HTML documents",
"NotSupportedError"
]);
}
if (data.includes("]]>")) {
throw DOMException.create(this._globalObject, [
"CDATA section data cannot contain the string \"]]>\"",
"InvalidCharacterError"
]);
}
return CDATASection.createImpl(this._globalObject, [], {
ownerDocument: this,
data
});
}
createTextNode(data) {
return Text.createImpl(this._globalObject, [], {
ownerDocument: this,
data
});
}
createComment(data) {
return Comment.createImpl(this._globalObject, [], {
ownerDocument: this,
data
});
}
// https://dom.spec.whatwg.org/#dom-document-createelement
createElement(localName, options) {
validateName(this._globalObject, localName);
if (this._parsingMode === "html") {
localName = asciiLowercase(localName);
}
let isValue = null;
if (options && options.is !== undefined) {
isValue = options.is;
}
const namespace = this._parsingMode === "html" || this.contentType === "application/xhtml+xml" ? HTML_NS : null;
return createElement(this, localName, namespace, null, isValue, true);
}
// https://dom.spec.whatwg.org/#dom-document-createelementns
createElementNS(namespace, qualifiedName, options) {
return internalCreateElementNSSteps(this, namespace, qualifiedName, options);
}
createDocumentFragment() {
return DocumentFragment.createImpl(this._globalObject, [], { ownerDocument: this });
}
createAttribute(localName) {
validateName(this._globalObject, localName);
if (this._parsingMode === "html") {
localName = asciiLowercase(localName);
}
return this._createAttribute({ localName });
}
createAttributeNS(namespace, name) {
if (namespace === undefined) {
namespace = null;
}
namespace = namespace !== null ? String(namespace) : namespace;
const extracted = validateAndExtract(this._globalObject, namespace, name);
return this._createAttribute({
namespace: extracted.namespace,
namespacePrefix: extracted.prefix,
localName: extracted.localName
});
}
// Using this helper function rather than directly calling generatedAttr.createImpl may be preferred in some files,
// to avoid introducing a potentially cyclic dependency on generated/Attr.js.
_createAttribute({
localName,
value,
namespace,
namespacePrefix
}) {
return generatedAttr.createImpl(this._globalObject, [], {
localName,
value,
namespace,
namespacePrefix,
ownerDocument: this
});
}
// TODO: Add callback interface support to `webidl2js`
createTreeWalker(root, whatToShow, filter) {
return TreeWalker.createImpl(this._globalObject, [], { root, whatToShow, filter });
}
createNodeIterator(root, whatToShow, filter) {
const nodeIterator = NodeIterator.createImpl(this._globalObject, [], { root, whatToShow, filter });
this._workingNodeIterators.push(nodeIterator);
while (this._workingNodeIterators.length > this._workingNodeIteratorsMax) {
const toInactivate = this._workingNodeIterators.shift();
toInactivate._working = false;
}
return nodeIterator;
}
importNode(node, deep) {
if (node.nodeType === NODE_TYPE.DOCUMENT_NODE) {
throw DOMException.create(this._globalObject, [
"Cannot import a document node",
"NotSupportedError"
]);
} else if (ShadowRoot.isImpl(node)) {
throw DOMException.create(this._globalObject, [
"Cannot adopt a shadow root",
"NotSupportedError"
]);
}
return clone(node, this, deep);
}
// https://dom.spec.whatwg.org/#dom-document-adoptnode
adoptNode(node) {
if (node.nodeType === NODE_TYPE.DOCUMENT_NODE) {
throw DOMException.create(this._globalObject, [
"Cannot adopt a document node",
"NotSupportedError"
]);
} else if (ShadowRoot.isImpl(node)) {
throw DOMException.create(this._globalObject, [
"Cannot adopt a shadow root",
"HierarchyRequestError"
]);
}
this._adoptNode(node);
return node;
}
// https://dom.spec.whatwg.org/#concept-node-adopt
_adoptNode(node) {
const newDocument = this;
const oldDocument = node._ownerDocument;
const parent = domSymbolTree.parent(node);
if (parent) {
parent._remove(node);
}
if (oldDocument !== newDocument) {
for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) {
inclusiveDescendant._ownerDocument = newDocument;
}
for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) {
if (inclusiveDescendant._ceState === "custom") {
enqueueCECallbackReaction(inclusiveDescendant, "adoptedCallback", [
idlUtils.wrapperForImpl(oldDocument),
idlUtils.wrapperForImpl(newDocument)
]);
}
}
for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) {
if (inclusiveDescendant._adoptingSteps) {
inclusiveDescendant._adoptingSteps(oldDocument);
}
}
}
}
get cookie() {
return this._cookieJar.getCookieStringSync(this.URL, { http: false });
}
set cookie(cookieStr) {
cookieStr = String(cookieStr);
this._cookieJar.setCookieSync(cookieStr, this.URL, {
http: false,
ignoreError: true
});
}
// The clear(), captureEvents(), and releaseEvents() methods must do nothing
clear() {}
captureEvents() {}
releaseEvents() {}
get styleSheets() {
if (!this._styleSheets) {
this._styleSheets = StyleSheetList.createImpl(this._globalObject);
}
// TODO: each style and link element should register its sheet on creation
// and remove it on removal.
return this._styleSheets;
}
get hidden() {
if (this._defaultView && this._defaultView._pretendToBeVisual) {
return false;
}
return true;
}
get visibilityState() {
if (this._defaultView && this._defaultView._pretendToBeVisual) {
return "visible";
}
return "prerender";
}
// https://w3c.github.io/selection-api/#extensions-to-document-interface
getSelection() {
return this._defaultView ? this._defaultView._selection : null;
}
// Needed to ensure that the resulting document has the correct prototype chain:
// https://dom.spec.whatwg.org/#concept-node-clone says "that implements the same interfaces as node".
_cloneDocument() {
const copy = documents.createImpl(
this._globalObject,
{
contentType: this.contentType,
encoding: this._encoding,
parsingMode: this._parsingMode
}
);
copy._URL = this._URL;
copy._origin = this._origin;
return copy;
}
}
eventAccessors.createEventAccessor(DocumentImpl.prototype, "readystatechange");
mixin(DocumentImpl.prototype, DocumentOrShadowRootImpl.prototype);
mixin(DocumentImpl.prototype, GlobalEventHandlersImpl.prototype);
mixin(DocumentImpl.prototype, NonElementParentNodeImpl.prototype);
mixin(DocumentImpl.prototype, ParentNodeImpl.prototype);
DocumentImpl.prototype.getElementsByTagName = memoizeQuery(function (qualifiedName) {
return listOfElementsWithQualifiedName(qualifiedName, this);
});
DocumentImpl.prototype.getElementsByTagNameNS = memoizeQuery(function (namespace, localName) {
return listOfElementsWithNamespaceAndLocalName(namespace, localName, this);
});
DocumentImpl.prototype.getElementsByClassName = memoizeQuery(function getElementsByClassName(classNames) {
return listOfElementsWithClassNames(classNames, this);
});
module.exports = {
implementation: DocumentImpl
};