UNPKG

jsdom

Version:

A JavaScript implementation of many web standards

1,184 lines (997 loc) • 36.9 kB
"use strict"; const { CookieJar } = require("tough-cookie"); const { DOMSelector } = require("@asamuzakjp/dom-selector"); const NodeImpl = require("./Node-impl").implementation; const idlUtils = require("../../../generated/idl/utils"); const NODE_TYPE = require("../node-type"); const { mixin } = require("../../utils"); const { firstChildWithLocalName, firstChildWithLocalNames, firstDescendantWithLocalName } = require("../helpers/traversal"); const whatwgURL = require("whatwg-url"); const StyleSheetList = require("../../../generated/idl/StyleSheetList.js"); 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("../../../generated/idl/DOMException"); const { parseIntoDocument } = require("../../browser/parser"); const History = require("../../../generated/idl/History"); const Location = require("../../../generated/idl/Location"); const HTMLCollection = require("../../../generated/idl/HTMLCollection"); const NodeList = require("../../../generated/idl/NodeList"); const { attributeLocalName: validateAttributeLocalName, elementLocalName: validateElementLocalName, validateAndExtract, xmlName: validateXMLName } = require("../helpers/validate-names"); const { fireAnEvent } = require("../helpers/events"); const { enqueueCECallbackReaction } = require("../helpers/custom-elements"); const { createElement, internalCreateElementNSSteps } = require("../helpers/create-element"); const IterableWeakSet = require("../helpers/iterable-weak-set"); const WeakValueMap = require("../helpers/weak-value-map"); 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 } = require("../node"); const generatedAttr = require("../../../generated/idl/Attr"); const Comment = require("../../../generated/idl/Comment"); const ProcessingInstruction = require("../../../generated/idl/ProcessingInstruction"); const CDATASection = require("../../../generated/idl/CDATASection"); const Text = require("../../../generated/idl/Text"); const DocumentFragment = require("../../../generated/idl/DocumentFragment"); const DOMImplementation = require("../../../generated/idl/DOMImplementation"); const TreeWalker = require("../../../generated/idl/TreeWalker"); const NodeIterator = require("../../../generated/idl/NodeIterator"); const ShadowRoot = require("../../../generated/idl/ShadowRoot"); const Range = require("../../../generated/idl/Range"); const documents = require("../documents.js"); const BeforeUnloadEvent = require("../../../generated/idl/BeforeUnloadEvent"); const CompositionEvent = require("../../../generated/idl/CompositionEvent"); const CustomEvent = require("../../../generated/idl/CustomEvent"); const DeviceMotionEvent = require("../../../generated/idl/DeviceMotionEvent"); const DeviceOrientationEvent = require("../../../generated/idl/DeviceOrientationEvent"); const Event = require("../../../generated/idl/Event"); const FocusEvent = require("../../../generated/idl/FocusEvent"); const HashChangeEvent = require("../../../generated/idl/HashChangeEvent"); const KeyboardEvent = require("../../../generated/idl/KeyboardEvent"); const MessageEvent = require("../../../generated/idl/MessageEvent"); const MouseEvent = require("../../../generated/idl/MouseEvent"); const StorageEvent = require("../../../generated/idl/StorageEvent"); const TouchEvent = require("../../../generated/idl/TouchEvent"); const UIEvent = require("../../../generated/idl/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"); const ByIdCache = require("../helpers/by-id-cache"); const { NAMED_PROPERTY_LOCAL_NAMES, isNamedPropertyElement } = require("../helpers/document-named-properties"); function clearChildNodes(node) { for (let child = node.firstChild; child; child = node.firstChild) { node.removeChild(child); } } function hasNonemptyAttribute(element, name) { const value = element.getAttributeNS(null, name); return value !== null && value !== ""; } function canContributeIdToNamedProperties(element, hasName) { return element._localName === "object" || (element._localName === "img" && hasName); } function attributeAffectsNamedProperties(element, name, value, oldValue) { if (element._namespaceURI !== HTML_NS || !element._isInDocumentTree || value === oldValue) { return false; } const hasChangedValue = (value !== null && value !== "") || (oldValue !== null && oldValue !== ""); if (!hasChangedValue) { return false; } if (name === "name") { return NAMED_PROPERTY_LOCAL_NAMES.has(element._localName); } if (name !== "id") { return false; } const hasName = hasNonemptyAttribute(element, "name"); return canContributeIdToNamedProperties(element, hasName); } 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 = { beforeunloadevent: BeforeUnloadEvent, compositionevent: CompositionEvent, customevent: CustomEvent, devicemotionevent: DeviceMotionEvent, deviceorientationevent: DeviceOrientationEvent, event: Event, events: Event, focusevent: FocusEvent, hashchangeevent: HashChangeEvent, htmlevents: Event, keyboardevent: KeyboardEvent, messageevent: MessageEvent, mouseevent: MouseEvent, mouseevents: MouseEvent, storageevent: StorageEvent, svgevents: Event, touchevent: TouchEvent, uievent: UIEvent, uievents: UIEvent }; class DocumentImpl extends NodeImpl { #domSelector = null; #lastFocusedElement = null; #namedPropertyCollections; #namedPropertyElementsCache = null; constructor(globalObject, args, { options: { parsingMode = "xml", encoding = "UTF-8", contentType = parsingMode === "xml" ? "application/xml" : "text/html", defaultView = null, cookieJar = new CookieJar(null, { looseMode: true }), parseOptions, scriptingDisabled = false, url: urlOption = "about:blank", parentOrigin, referrer = "", lastModified = new Date(), readyState = "complete" } = {}, ...privateData }) { super(globalObject, args, privateData); this._initGlobalEvents(); this._ownerDocument = this; this.nodeType = NODE_TYPE.DOCUMENT_NODE; this._parsingMode = parsingMode; this._implementation = DOMImplementation.createImpl(this._globalObject, [], { ownerDocument: this }); this._defaultView = defaultView; this._byIdCache = new ByIdCache(this); this._isInDocumentTree = true; this._currentScript = null; this._pageShowingFlag = false; this._cookieJar = cookieJar; this._parseOptions = { ...parseOptions }; this._scriptingDisabled = scriptingDisabled; if (this._scriptingDisabled) { this._parseOptions.scriptingEnabled = false; } this.contentType = contentType; this._encoding = encoding; const parsed = whatwgURL.parseURL(urlOption); if (parsed === null) { throw new TypeError(`Could not parse "${urlOption}" as a URL`); } this._URL = parsed; this._origin = urlOption === "about:blank" && parentOrigin ? parentOrigin : whatwgURL.serializeURLOrigin(this._URL); 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 = new IterableWeakSet(); this._referrer = referrer; this._lastModified = toLastModifiedString(lastModified); this._asyncQueue = new AsyncResourceQueue(); this._queue = new ResourceQueue({ asyncQueue: this._asyncQueue, paused: false }); this._deferQueue = new ResourceQueue({ paused: true }); this._requestManager = new RequestManager(); // Set when destruction begins, before reentrant cleanup. This is not `window.closed` or the inverse of // "fully active": an inactive document need not be destroyed. // https://html.spec.whatwg.org/multipage/document-lifecycle.html#destroy-a-document this._isDestroyed = false; this._childDocuments = new Set(); this._parentDocument = null; this._currentDocumentReadiness = readyState; 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; // Cache of computed element styles this._styleCache = new WeakMap(); // Cache of document base URL this._baseURLCache = null; this._baseURLSerializedCache = null; } _clearStyleCache() { this._styleCache = new WeakMap(); } get _lastFocusedElement() { return this.#lastFocusedElement; } set _lastFocusedElement(element) { if (this.#lastFocusedElement !== element) { this.#lastFocusedElement = element; this._invalidateSelectorState(); } } // The `DOMSelector` instance is lazily created, as it is somewhat expensive to create and not always needed. _getDOMSelector() { if (!this.#domSelector) { this.#domSelector = new DOMSelector(this._globalObject, this._ownerDocument, { idlUtils }); } return this.#domSelector; } _clearDOMSelector() { this.#domSelector?.clear(); } _clearBaseURLCache() { this._baseURLCache = null; this._baseURLSerializedCache = null; } // https://html.spec.whatwg.org/multipage/infrastructure.html#document-base-url baseURL() { if (this._baseURLCache) { return this._baseURLCache; } const firstBase = this.querySelector("base[href]"); this._baseURLCache = firstBase === null ? this._fallbackBaseURL() : this._frozenBaseURL(firstBase, this._fallbackBaseURL()); return this._baseURLCache; } baseURLSerialized() { if (this._baseURLSerializedCache) { return this._baseURLSerializedCache; } const result = whatwgURL.serializeURL(this.baseURL()); this._baseURLSerializedCache = result; return result; } // https://html.spec.whatwg.org/#encoding-parsing-a-url encodingParseAURL(url) { return whatwgURL.parseURL(url, { baseURL: this.baseURL(), encoding: this._encoding }); } // https://html.spec.whatwg.org/#frozen-base-url _frozenBaseURL(baseElement, fallbackBaseURL) { // The spec is eager (setting the frozen base URL when things change); we are lazy (getting it when we need to). // // There is a slight difference, which is when history.pushState() is involved. The frozen base URL does not get // updated in response to history.pushState() per spec, but since we're lazy, it will get updated. // // The test in to-port-to-wpts/history.js checks for the current jsdom behavior (which is incorrect). // We could make it pass by not invalidating the base URL cache, actually. But that would just make the fallback // base URL case use the stale base URL. // // TODO: implement, with tests for all code paths, the spec's behavior. const baseHrefAttribute = baseElement.getAttributeNS(null, "href"); const urlRecord = whatwgURL.parseURL(baseHrefAttribute, { baseURL: fallbackBaseURL }); // data: and javascript: URLs are never usable as a base URL, so the fallback base URL is used // instead. Note this does not fall through to any later base element: the document base URL is // the first base element with an href, whether or not that element's URL turns out to be usable. // // We don't implement the "Is base allowed for Document?" CSP check. if (urlRecord === null || urlRecord.scheme === "data" || urlRecord.scheme === "javascript") { return fallbackBaseURL; } return urlRecord; } // https://html.spec.whatwg.org/#fallback-base-url _fallbackBaseURL() { if (this.URL === "about:blank" && this._defaultView && this._defaultView._parent !== this._defaultView) { return this._defaultView._parent._document.baseURL(); } return this._URL; } _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 this._children()) { 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() { return this.firstElementChild; } 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); } _clearNamedPropertyCache() { this.#namedPropertyElementsCache = null; } write(...args) { let text = ""; for (let i = 0; i < args.length; ++i) { text += args[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(...args) { this.write(...args, "\n"); } // This is implemented separately for Document (which has a _byIdCache) and DocumentFragment (which does not). getElementById(id) { return this._byIdCache.get(id); } 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: () => this._descendantsToArray(node => { return (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: () => this._descendantsToArray(node => { return 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 = this.firstChild; while (child) { this.removeChild(child); child = this.firstChild; } this._invalidateCaches(); return this; } close(noQueue) { if (this._isDestroyed) { return Promise.resolve(); } // 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 Promise.resolve(); } this._queue.resume(); const dummyPromise = Promise.resolve(); const { promise: completion, resolve: complete } = Promise.withResolvers(); const onDOMContentLoad = () => { const doc = this; function dispatchEvent() { if (doc._isDestroyed) { return; } // 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(); resolve(); return; } this._deferQueue.setListener(() => { dispatchEvent(); resolve(); }); this._deferQueue.resume(); }); }; const onLoad = () => { const doc = this; function dispatchEvent() { if (!doc._isDestroyed) { doc.readyState = "complete"; fireAnEvent("load", doc); } complete(); } return new Promise(resolve => { if (this._asyncQueue.count() === 0) { dispatchEvent(); resolve(); return; } this._asyncQueue.setListener(() => { dispatchEvent(); resolve(); }); }); }; this._queue.push(dummyPromise, onDOMContentLoad, null); // Complete after all resources, even when destruction suppresses the load event: frame loading must not wait // on an event that will never fire. this._queue.push(dummyPromise, onLoad, null, true); return completion; } // Queue document-associated work, discarding it on destruction. Callers can also pass an operation's signal; // stopping fetches must not discard unrelated tasks. The promise settles even when the task is discarded, so // callers can release resources in `finally`. This does not model task sources or waiting for a document to // become fully active. // https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-task _queueATask(steps, { signal } = {}) { return new Promise((resolve, reject) => { setImmediate(() => { if (this._isDestroyed || signal?.aborted) { resolve(); return; } try { resolve(steps()); } catch (error) { reject(error); } }); }); } /** * Named property name to the elements contributing it, in tree order. Tree insertions, removals and attribute changes * discard this cache only when they can change the map. * * https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter */ get #namedPropertyElements() { if (this.#namedPropertyElementsCache === null) { const map = new Map(); function add(name, element) { const elements = map.get(name); if (elements === undefined) { map.set(name, [element]); } else if (elements[elements.length - 1] !== element) { // Guards against an element whose id and name are equal contributing twice. elements.push(element); } } for (const element of this._descendantsToArray(isNamedPropertyElement)) { const name = element.getAttributeNS(null, "name"); const hasName = name !== null && name !== ""; const id = element.getAttributeNS(null, "id"); // An object element contributes its id; an img element only alongside a non-empty name. // The id is added first: the standard orders values from id attributes before values from // name attributes when the same element contributes both. if (id !== null && id !== "" && canContributeIdToNamedProperties(element, hasName)) { add(id, element); } if (hasName) { add(name, element); } } this.#namedPropertyElementsCache = map; } return this.#namedPropertyElementsCache; } _namedPropertyElementAttributeModified(element, name, value, oldValue) { if (this.#namedPropertyElementsCache !== null && attributeAffectsNamedProperties(element, name, value, oldValue)) { this.#namedPropertyElementsCache = null; } } get [idlUtils.supportedPropertyNames]() { return this.#namedPropertyElements.keys(); } [idlUtils.supportsPropertyName](name) { return this.#namedPropertyElements.has(name); } // https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem [idlUtils.namedGet](name) { const elements = this.#namedPropertyElements.get(name); if (elements === undefined) { return undefined; } if (elements.length === 1) { const [element] = elements; // A lone iframe answers with its content window rather than the element. if (element._localName === "iframe") { const { contentWindow } = element; if (contentWindow !== null) { return contentWindow; } } return element; } // Collections are per name and outlive the cache above, since the standard hands out the same // collection each time. The query reads the current map, so the collection stays live. this.#namedPropertyCollections ??= new WeakValueMap(); let collection = this.#namedPropertyCollections.get(name); if (collection === undefined) { collection = HTMLCollection.createImpl(this._globalObject, [], { element: this, query: () => this.#namedPropertyElements.get(name) ?? [] }); this.#namedPropertyCollections.set(name, collection); } return collection; } getElementsByName(elementName) { return NodeList.createImpl(this._globalObject, [], { element: this, query: () => this._descendantsToArray(node => { return 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); } 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) { validateXMLName(this._globalObject, target); if (data.includes("?>")) { throw DOMException.create(this._globalObject, [ "Processing instruction data cannot contain the string \"?>\"", "InvalidCharacterError" ]); } return this._createProcessingInstruction(target, data); } // Cloning existing processing instructions bypasses `createProcessingInstruction()`'s validation. _createProcessingInstruction(target, data) { 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 this._createCDATASection(data); } // Cloning existing CDATA sections bypasses `createCDATASection()`'s document and data restrictions. _createCDATASection(data) { 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) { validateElementLocalName(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 }); } // https://dom.spec.whatwg.org/#dom-document-createattribute createAttribute(localName) { validateAttributeLocalName(this._globalObject, localName); if (this._parsingMode === "html") { localName = asciiLowercase(localName); } return this._createAttribute({ localName }); } // https://dom.spec.whatwg.org/#dom-document-createattributens createAttributeNS(namespace, name) { if (namespace === undefined) { namespace = null; } namespace = namespace !== null ? String(namespace) : namespace; const extracted = validateAndExtract(this._globalObject, namespace, name, "attribute"); 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 }); } 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.add(nodeIterator); 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 = node.parentNode; if (parent) { parent._remove(node); } if (oldDocument !== newDocument) { for (const inclusiveDescendant of node._shadowIncludingInclusiveDescendants()) { inclusiveDescendant._ownerDocument = newDocument; if (inclusiveDescendant.nodeType === NODE_TYPE.ELEMENT_NODE) { for (const attribute of inclusiveDescendant._attributeList) { attribute._ownerDocument = newDocument; } } // Collections built before the move captured the old document's HTML-ness, and the spec // keeps those working. A fresh call has to see the new document, so clear the memoized ones. // It is this subtree that changed document; the old parent's collections are unaffected by the move. inclusiveDescendant._clearMemoizedQueries(); if (inclusiveDescendant._ceState === "custom") { enqueueCECallbackReaction(inclusiveDescendant, "adoptedCallback", [ idlUtils.wrapperForImpl(oldDocument), idlUtils.wrapperForImpl(newDocument) ]); } 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._settings.pretendToBeVisual) { return false; } return true; } get visibilityState() { if (this._defaultView && this._defaultView._settings.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); module.exports = { implementation: DocumentImpl };