diff options
Diffstat (limited to 'node_modules/jsdom/lib/jsdom/living/helpers')
32 files changed, 3595 insertions, 0 deletions
diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/agent-factory.js b/node_modules/jsdom/lib/jsdom/living/helpers/agent-factory.js new file mode 100644 index 0000000..4af6a24 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/agent-factory.js @@ -0,0 +1,15 @@ +"use strict"; +const http = require("http"); +const https = require("https"); +const { parse: parseURLToNodeOptions } = require("url"); +const HttpProxyAgent = require("http-proxy-agent"); +const HttpsProxyAgent = require("https-proxy-agent"); + +module.exports = function agentFactory(proxy, rejectUnauthorized) { + const agentOpts = { keepAlive: true, rejectUnauthorized }; + if (proxy) { + const proxyOpts = { ...parseURLToNodeOptions(proxy), ...agentOpts }; + return { https: new HttpsProxyAgent(proxyOpts), http: new HttpProxyAgent(proxyOpts) }; + } + return { http: new http.Agent(agentOpts), https: new https.Agent(agentOpts) }; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/binary-data.js b/node_modules/jsdom/lib/jsdom/living/helpers/binary-data.js new file mode 100644 index 0000000..dc5909c --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/binary-data.js @@ -0,0 +1,9 @@ +"use strict"; + +// See https://github.com/jsdom/jsdom/pull/2743#issuecomment-562991955 for background. +exports.copyToArrayBufferInNewRealm = (nodejsBuffer, newRealm) => { + const newAB = new newRealm.ArrayBuffer(nodejsBuffer.byteLength); + const view = new Uint8Array(newAB); + view.set(nodejsBuffer); + return newAB; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/create-element.js b/node_modules/jsdom/lib/jsdom/living/helpers/create-element.js new file mode 100644 index 0000000..0a330ec --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/create-element.js @@ -0,0 +1,320 @@ +"use strict"; + +const DOMException = require("domexception/webidl2js-wrapper"); + +const interfaces = require("../interfaces"); + +const { implForWrapper } = require("../generated/utils"); + +const { HTML_NS, SVG_NS } = require("./namespaces"); +const { domSymbolTree } = require("./internal-constants"); +const { validateAndExtract } = require("./validate-names"); +const reportException = require("./runtime-script-errors"); +const { + isValidCustomElementName, upgradeElement, lookupCEDefinition, enqueueCEUpgradeReaction +} = require("./custom-elements"); + +const INTERFACE_TAG_MAPPING = { + // https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom%3Aelement-interface + // https://html.spec.whatwg.org/multipage/indices.html#elements-3 + [HTML_NS]: { + HTMLElement: [ + "abbr", "address", "article", "aside", "b", "bdi", "bdo", "cite", "code", "dd", "dfn", "dt", "em", "figcaption", + "figure", "footer", "header", "hgroup", "i", "kbd", "main", "mark", "nav", "noscript", "rp", "rt", "ruby", "s", + "samp", "section", "small", "strong", "sub", "summary", "sup", "u", "var", "wbr" + ], + HTMLAnchorElement: ["a"], + HTMLAreaElement: ["area"], + HTMLAudioElement: ["audio"], + HTMLBaseElement: ["base"], + HTMLBodyElement: ["body"], + HTMLBRElement: ["br"], + HTMLButtonElement: ["button"], + HTMLCanvasElement: ["canvas"], + HTMLDataElement: ["data"], + HTMLDataListElement: ["datalist"], + HTMLDetailsElement: ["details"], + HTMLDialogElement: ["dialog"], + HTMLDirectoryElement: ["dir"], + HTMLDivElement: ["div"], + HTMLDListElement: ["dl"], + HTMLEmbedElement: ["embed"], + HTMLFieldSetElement: ["fieldset"], + HTMLFontElement: ["font"], + HTMLFormElement: ["form"], + HTMLFrameElement: ["frame"], + HTMLFrameSetElement: ["frameset"], + HTMLHeadingElement: ["h1", "h2", "h3", "h4", "h5", "h6"], + HTMLHeadElement: ["head"], + HTMLHRElement: ["hr"], + HTMLHtmlElement: ["html"], + HTMLIFrameElement: ["iframe"], + HTMLImageElement: ["img"], + HTMLInputElement: ["input"], + HTMLLabelElement: ["label"], + HTMLLegendElement: ["legend"], + HTMLLIElement: ["li"], + HTMLLinkElement: ["link"], + HTMLMapElement: ["map"], + HTMLMarqueeElement: ["marquee"], + HTMLMediaElement: [], + HTMLMenuElement: ["menu"], + HTMLMetaElement: ["meta"], + HTMLMeterElement: ["meter"], + HTMLModElement: ["del", "ins"], + HTMLObjectElement: ["object"], + HTMLOListElement: ["ol"], + HTMLOptGroupElement: ["optgroup"], + HTMLOptionElement: ["option"], + HTMLOutputElement: ["output"], + HTMLParagraphElement: ["p"], + HTMLParamElement: ["param"], + HTMLPictureElement: ["picture"], + HTMLPreElement: ["listing", "pre", "xmp"], + HTMLProgressElement: ["progress"], + HTMLQuoteElement: ["blockquote", "q"], + HTMLScriptElement: ["script"], + HTMLSelectElement: ["select"], + HTMLSlotElement: ["slot"], + HTMLSourceElement: ["source"], + HTMLSpanElement: ["span"], + HTMLStyleElement: ["style"], + HTMLTableCaptionElement: ["caption"], + HTMLTableCellElement: ["th", "td"], + HTMLTableColElement: ["col", "colgroup"], + HTMLTableElement: ["table"], + HTMLTimeElement: ["time"], + HTMLTitleElement: ["title"], + HTMLTableRowElement: ["tr"], + HTMLTableSectionElement: ["thead", "tbody", "tfoot"], + HTMLTemplateElement: ["template"], + HTMLTextAreaElement: ["textarea"], + HTMLTrackElement: ["track"], + HTMLUListElement: ["ul"], + HTMLUnknownElement: [], + HTMLVideoElement: ["video"] + }, + [SVG_NS]: { + SVGElement: [], + SVGGraphicsElement: [], + SVGSVGElement: ["svg"], + SVGTitleElement: ["title"] + } +}; + +const TAG_INTERFACE_LOOKUP = {}; + +for (const namespace of [HTML_NS, SVG_NS]) { + TAG_INTERFACE_LOOKUP[namespace] = {}; + + const interfaceNames = Object.keys(INTERFACE_TAG_MAPPING[namespace]); + for (const interfaceName of interfaceNames) { + const tagNames = INTERFACE_TAG_MAPPING[namespace][interfaceName]; + + for (const tagName of tagNames) { + TAG_INTERFACE_LOOKUP[namespace][tagName] = interfaceName; + } + } +} + +const UNKNOWN_HTML_ELEMENTS_NAMES = ["applet", "bgsound", "blink", "isindex", "keygen", "multicol", "nextid", "spacer"]; +const HTML_ELEMENTS_NAMES = [ + "acronym", "basefont", "big", "center", "nobr", "noembed", "noframes", "plaintext", "rb", "rtc", + "strike", "tt" +]; + +// https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom:element-interface +function getHTMLElementInterface(name) { + if (UNKNOWN_HTML_ELEMENTS_NAMES.includes(name)) { + return interfaces.getInterfaceWrapper("HTMLUnknownElement"); + } + + if (HTML_ELEMENTS_NAMES.includes(name)) { + return interfaces.getInterfaceWrapper("HTMLElement"); + } + + const specDefinedInterface = TAG_INTERFACE_LOOKUP[HTML_NS][name]; + if (specDefinedInterface !== undefined) { + return interfaces.getInterfaceWrapper(specDefinedInterface); + } + + if (isValidCustomElementName(name)) { + return interfaces.getInterfaceWrapper("HTMLElement"); + } + + return interfaces.getInterfaceWrapper("HTMLUnknownElement"); +} + +// https://svgwg.org/svg2-draft/types.html#ElementsInTheSVGDOM +function getSVGInterface(name) { + const specDefinedInterface = TAG_INTERFACE_LOOKUP[SVG_NS][name]; + if (specDefinedInterface !== undefined) { + return interfaces.getInterfaceWrapper(specDefinedInterface); + } + + return interfaces.getInterfaceWrapper("SVGElement"); +} + +// Returns the list of valid tag names that can bo associated with a element given its namespace and name. +function getValidTagNames(namespace, name) { + if (INTERFACE_TAG_MAPPING[namespace] && INTERFACE_TAG_MAPPING[namespace][name]) { + return INTERFACE_TAG_MAPPING[namespace][name]; + } + + return []; +} + +// https://dom.spec.whatwg.org/#concept-create-element +function createElement( + document, + localName, + namespace, + prefix = null, + isValue = null, + synchronousCE = false +) { + let result = null; + + const { _globalObject } = document; + const definition = lookupCEDefinition(document, namespace, localName, isValue); + + if (definition !== null && definition.name !== localName) { + const elementInterface = getHTMLElementInterface(localName); + + result = elementInterface.createImpl(_globalObject, [], { + ownerDocument: document, + localName, + namespace: HTML_NS, + prefix, + ceState: "undefined", + ceDefinition: null, + isValue + }); + + if (synchronousCE) { + upgradeElement(definition, result); + } else { + enqueueCEUpgradeReaction(result, definition); + } + } else if (definition !== null) { + if (synchronousCE) { + try { + const C = definition.constructor; + + const resultWrapper = C.construct(); + result = implForWrapper(resultWrapper); + + if (!result._ceState || !result._ceDefinition || result._namespaceURI !== HTML_NS) { + throw new TypeError("Internal error: Invalid custom element."); + } + + if (result._attributeList.length !== 0) { + throw DOMException.create(_globalObject, ["Unexpected attributes.", "NotSupportedError"]); + } + if (domSymbolTree.hasChildren(result)) { + throw DOMException.create(_globalObject, ["Unexpected child nodes.", "NotSupportedError"]); + } + if (domSymbolTree.parent(result)) { + throw DOMException.create(_globalObject, ["Unexpected element parent.", "NotSupportedError"]); + } + if (result._ownerDocument !== document) { + throw DOMException.create(_globalObject, ["Unexpected element owner document.", "NotSupportedError"]); + } + if (result._namespaceURI !== namespace) { + throw DOMException.create(_globalObject, ["Unexpected element namespace URI.", "NotSupportedError"]); + } + if (result._localName !== localName) { + throw DOMException.create(_globalObject, ["Unexpected element local name.", "NotSupportedError"]); + } + + result._prefix = prefix; + result._isValue = isValue; + } catch (error) { + reportException(document._defaultView, error); + + const interfaceWrapper = interfaces.getInterfaceWrapper("HTMLUnknownElement"); + result = interfaceWrapper.createImpl(_globalObject, [], { + ownerDocument: document, + localName, + namespace: HTML_NS, + prefix, + ceState: "failed", + ceDefinition: null, + isValue: null + }); + } + } else { + const interfaceWrapper = interfaces.getInterfaceWrapper("HTMLElement"); + result = interfaceWrapper.createImpl(_globalObject, [], { + ownerDocument: document, + localName, + namespace: HTML_NS, + prefix, + ceState: "undefined", + ceDefinition: null, + isValue: null + }); + + enqueueCEUpgradeReaction(result, definition); + } + } else { + let elementInterface; + + switch (namespace) { + case HTML_NS: + elementInterface = getHTMLElementInterface(localName); + break; + + case SVG_NS: + elementInterface = getSVGInterface(localName); + break; + + default: + elementInterface = interfaces.getInterfaceWrapper("Element"); + break; + } + + result = elementInterface.createImpl(_globalObject, [], { + ownerDocument: document, + localName, + namespace, + prefix, + ceState: "uncustomized", + ceDefinition: null, + isValue + }); + + if (namespace === HTML_NS && (isValidCustomElementName(localName) || isValue !== null)) { + result._ceState = "undefined"; + } + } + + return result; +} + +// https://dom.spec.whatwg.org/#internal-createelementns-steps +function internalCreateElementNSSteps(document, namespace, qualifiedName, options) { + const extracted = validateAndExtract(document._globalObject, namespace, qualifiedName); + + let isValue = null; + if (options && options.is !== undefined) { + isValue = options.is; + } + + return createElement( + document, + extracted.localName, + extracted.namespace, + extracted.prefix, + isValue, + true + ); +} + +module.exports = { + createElement, + internalCreateElementNSSteps, + + getValidTagNames, + getHTMLElementInterface +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/create-event-accessor.js b/node_modules/jsdom/lib/jsdom/living/helpers/create-event-accessor.js new file mode 100644 index 0000000..b46e2ae --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/create-event-accessor.js @@ -0,0 +1,188 @@ +"use strict"; + +const idlUtils = require("../generated/utils"); +const ErrorEvent = require("../generated/ErrorEvent"); +const EventHandlerNonNull = require("../generated/EventHandlerNonNull.js"); +const OnBeforeUnloadEventHandlerNonNull = require("../generated/OnBeforeUnloadEventHandlerNonNull.js"); +const OnErrorEventHandlerNonNull = require("../generated/OnErrorEventHandlerNonNull.js"); +const reportException = require("./runtime-script-errors"); + +exports.appendHandler = (el, eventName) => { + // tryImplForWrapper() is currently required due to use in Window.js + idlUtils.tryImplForWrapper(el).addEventListener(eventName, event => { + // https://html.spec.whatwg.org/#the-event-handler-processing-algorithm + const callback = exports.getCurrentEventHandlerValue(el, eventName); + if (callback === null) { + return; + } + + const specialError = ErrorEvent.isImpl(event) && event.type === "error" && + event.currentTarget.constructor.name === "Window"; + + let returnValue = null; + // https://heycam.github.io/webidl/#es-invoking-callback-functions + if (typeof callback === "function") { + if (specialError) { + returnValue = callback.call( + event.currentTarget, + event.message, + event.filename, + event.lineno, + event.colno, + event.error + ); + } else { + returnValue = callback.call(event.currentTarget, event); + } + } + + // TODO: we don't implement BeforeUnloadEvent so we can't brand-check here + if (event.type === "beforeunload") { + if (returnValue !== null) { + event._canceledFlag = true; + if (event.returnValue === "") { + event.returnValue = returnValue; + } + } + } else if (specialError) { + if (returnValue === true) { + event._canceledFlag = true; + } + } else if (returnValue === false) { + event._canceledFlag = true; + } + }); +}; + +// "Simple" in this case means "no content attributes involved" +exports.setupForSimpleEventAccessors = (prototype, events) => { + prototype._getEventHandlerFor = function (event) { + return this._eventHandlers ? this._eventHandlers[event] : undefined; + }; + + prototype._setEventHandlerFor = function (event, handler) { + if (!this._registeredHandlers) { + this._registeredHandlers = new Set(); + this._eventHandlers = Object.create(null); + } + + if (!this._registeredHandlers.has(event) && handler !== null) { + this._registeredHandlers.add(event); + exports.appendHandler(this, event); + } + this._eventHandlers[event] = handler; + }; + + for (const event of events) { + exports.createEventAccessor(prototype, event); + } +}; + +// https://html.spec.whatwg.org/multipage/webappapis.html#getting-the-current-value-of-the-event-handler +exports.getCurrentEventHandlerValue = (target, event) => { + const value = target._getEventHandlerFor(event); + if (!value) { + return null; + } + + if (value.body !== undefined) { + let element, document, fn; + if (target.constructor.name === "Window") { + element = null; + document = idlUtils.implForWrapper(target.document); + } else { + element = target; + document = element.ownerDocument; + } + const { body } = value; + + const formOwner = element !== null && element.form ? element.form : null; + const window = target.constructor.name === "Window" && target._document ? target : document.defaultView; + + try { + // eslint-disable-next-line no-new-func + Function(body); // properly error out on syntax errors + // Note: this won't execute body; that would require `Function(body)()`. + } catch (e) { + if (window) { + reportException(window, e); + } + target._setEventHandlerFor(event, null); + return null; + } + + // Note: the with (window) { } is not necessary in Node, but is necessary in a browserified environment. + + const createFunction = document.defaultView.Function; + if (event === "error" && element === null) { + const sourceURL = document ? `\n//# sourceURL=${document.URL}` : ""; + + fn = createFunction(`\ +with (arguments[0]) { return function onerror(event, source, lineno, colno, error) { +${body} +}; }${sourceURL}`)(window); + + fn = OnErrorEventHandlerNonNull.convert(fn); + } else { + const calls = []; + if (element !== null) { + calls.push(idlUtils.wrapperForImpl(document)); + } + + if (formOwner !== null) { + calls.push(idlUtils.wrapperForImpl(formOwner)); + } + + if (element !== null) { + calls.push(idlUtils.wrapperForImpl(element)); + } + + let wrapperBody = `\ +with (arguments[0]) { return function on${event}(event) { +${body} +}; }`; + + // eslint-disable-next-line no-unused-vars + for (const call of calls) { + wrapperBody = `\ +with (arguments[0]) { return function () { +${wrapperBody} +}; }`; + } + + if (document) { + wrapperBody += `\n//# sourceURL=${document.URL}`; + } + + fn = createFunction(wrapperBody)(window); + for (const call of calls) { + fn = fn(call); + } + + if (event === "beforeunload") { + fn = OnBeforeUnloadEventHandlerNonNull.convert(fn); + } else { + fn = EventHandlerNonNull.convert(fn); + } + } + + target._setEventHandlerFor(event, fn); + } + + return target._getEventHandlerFor(event); +}; + +// https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-idl-attributes +// TODO: Consider replacing this with `[ReflectEvent]` +exports.createEventAccessor = (obj, event) => { + Object.defineProperty(obj, "on" + event, { + configurable: true, + enumerable: true, + get() { + return exports.getCurrentEventHandlerValue(this, event); + }, + set(val) { + this._setEventHandlerFor(event, val); + } + }); +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/custom-elements.js b/node_modules/jsdom/lib/jsdom/living/helpers/custom-elements.js new file mode 100644 index 0000000..1dbd773 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/custom-elements.js @@ -0,0 +1,270 @@ +"use strict"; + +const DOMException = require("domexception/webidl2js-wrapper"); +const isPotentialCustomElementName = require("is-potential-custom-element-name"); + +const NODE_TYPE = require("../node-type"); +const { HTML_NS } = require("./namespaces"); +const { shadowIncludingRoot } = require("./shadow-dom"); +const reportException = require("./runtime-script-errors"); + +const { implForWrapper, wrapperForImpl } = require("../generated/utils"); + +// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions-stack +class CEReactionsStack { + constructor() { + this._stack = []; + + // https://html.spec.whatwg.org/multipage/custom-elements.html#backup-element-queue + this.backupElementQueue = []; + + // https://html.spec.whatwg.org/multipage/custom-elements.html#processing-the-backup-element-queue + this.processingBackupElementQueue = false; + } + + push(elementQueue) { + this._stack.push(elementQueue); + } + + pop() { + return this._stack.pop(); + } + + get currentElementQueue() { + const { _stack } = this; + return _stack[_stack.length - 1]; + } + + isEmpty() { + return this._stack.length === 0; + } +} + +// In theory separate cross-origin Windows created by separate JSDOM instances could have separate stacks. But, we would +// need to implement the whole agent architecture. Which is kind of questionable given that we don't run our Windows in +// their own separate threads, which is what agents are meant to represent. +const customElementReactionsStack = new CEReactionsStack(); + +// https://html.spec.whatwg.org/multipage/custom-elements.html#cereactions +function ceReactionsPreSteps() { + customElementReactionsStack.push([]); +} +function ceReactionsPostSteps() { + const queue = customElementReactionsStack.pop(); + invokeCEReactions(queue); +} + +const RESTRICTED_CUSTOM_ELEMENT_NAME = new Set([ + "annotation-xml", + "color-profile", + "font-face", + "font-face-src", + "font-face-uri", + "font-face-format", + "font-face-name", + "missing-glyph" +]); + +// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name +function isValidCustomElementName(name) { + if (RESTRICTED_CUSTOM_ELEMENT_NAME.has(name)) { + return false; + } + + return isPotentialCustomElementName(name); +} + +// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-upgrade-an-element +function upgradeElement(definition, element) { + if (element._ceState !== "undefined" || element._ceState === "uncustomized") { + return; + } + + element._ceDefinition = definition; + element._ceState = "failed"; + + for (const attribute of element._attributeList) { + const { _localName, _namespace, _value } = attribute; + enqueueCECallbackReaction(element, "attributeChangedCallback", [_localName, null, _value, _namespace]); + } + + if (shadowIncludingRoot(element).nodeType === NODE_TYPE.DOCUMENT_NODE) { + enqueueCECallbackReaction(element, "connectedCallback", []); + } + + definition.constructionStack.push(element); + + const { constructionStack, constructor: C } = definition; + + let constructionError; + try { + if (definition.disableShadow === true && element._shadowRoot !== null) { + throw DOMException.create(element._globalObject, [ + "Can't upgrade a custom element with a shadow root if shadow is disabled", + "NotSupportedError" + ]); + } + + const constructionResult = C.construct(); + const constructionResultImpl = implForWrapper(constructionResult); + + if (constructionResultImpl !== element) { + throw new TypeError("Invalid custom element constructor return value"); + } + } catch (error) { + constructionError = error; + } + + constructionStack.pop(); + + if (constructionError !== undefined) { + element._ceDefinition = null; + element._ceReactionQueue = []; + + throw constructionError; + } + + element._ceState = "custom"; +} + +// https://html.spec.whatwg.org/#concept-try-upgrade +function tryUpgradeElement(element) { + const { _ownerDocument, _namespaceURI, _localName, _isValue } = element; + const definition = lookupCEDefinition(_ownerDocument, _namespaceURI, _localName, _isValue); + + if (definition !== null) { + enqueueCEUpgradeReaction(element, definition); + } +} + +// https://html.spec.whatwg.org/#look-up-a-custom-element-definition +function lookupCEDefinition(document, namespace, localName, isValue) { + const definition = null; + + if (namespace !== HTML_NS) { + return definition; + } + + if (!document._defaultView) { + return definition; + } + + const registry = implForWrapper(document._globalObject.customElements); + + const definitionByName = registry._customElementDefinitions.find(def => { + return def.name === def.localName && def.localName === localName; + }); + if (definitionByName !== undefined) { + return definitionByName; + } + + const definitionByIs = registry._customElementDefinitions.find(def => { + return def.name === isValue && def.localName === localName; + }); + if (definitionByIs !== undefined) { + return definitionByIs; + } + + return definition; +} + +// https://html.spec.whatwg.org/multipage/custom-elements.html#invoke-custom-element-reactions +function invokeCEReactions(elementQueue) { + while (elementQueue.length > 0) { + const element = elementQueue.shift(); + + const reactions = element._ceReactionQueue; + + try { + while (reactions.length > 0) { + const reaction = reactions.shift(); + + switch (reaction.type) { + case "upgrade": + upgradeElement(reaction.definition, element); + break; + + case "callback": + reaction.callback.apply(wrapperForImpl(element), reaction.args); + break; + } + } + } catch (error) { + reportException(element._globalObject, error); + } + } +} + +// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-an-element-on-the-appropriate-element-queue +function enqueueElementOnAppropriateElementQueue(element) { + if (customElementReactionsStack.isEmpty()) { + customElementReactionsStack.backupElementQueue.push(element); + + if (customElementReactionsStack.processingBackupElementQueue) { + return; + } + + customElementReactionsStack.processingBackupElementQueue = true; + + Promise.resolve().then(() => { + const elementQueue = customElementReactionsStack.backupElementQueue; + invokeCEReactions(elementQueue); + + customElementReactionsStack.processingBackupElementQueue = false; + }); + } else { + customElementReactionsStack.currentElementQueue.push(element); + } +} + +// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-a-custom-element-callback-reaction +function enqueueCECallbackReaction(element, callbackName, args) { + const { _ceDefinition: { lifecycleCallbacks, observedAttributes } } = element; + + const callback = lifecycleCallbacks[callbackName]; + if (callback === null) { + return; + } + + if (callbackName === "attributeChangedCallback") { + const attributeName = args[0]; + if (!observedAttributes.includes(attributeName)) { + return; + } + } + + element._ceReactionQueue.push({ + type: "callback", + callback, + args + }); + + enqueueElementOnAppropriateElementQueue(element); +} + +// https://html.spec.whatwg.org/#enqueue-a-custom-element-upgrade-reaction +function enqueueCEUpgradeReaction(element, definition) { + element._ceReactionQueue.push({ + type: "upgrade", + definition + }); + + enqueueElementOnAppropriateElementQueue(element); +} + +module.exports = { + customElementReactionsStack, + + ceReactionsPreSteps, + ceReactionsPostSteps, + + isValidCustomElementName, + + upgradeElement, + tryUpgradeElement, + + lookupCEDefinition, + enqueueCEUpgradeReaction, + enqueueCECallbackReaction, + invokeCEReactions +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/dates-and-times.js b/node_modules/jsdom/lib/jsdom/living/helpers/dates-and-times.js new file mode 100644 index 0000000..15d920b --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/dates-and-times.js @@ -0,0 +1,270 @@ +"use strict"; + +function isLeapYear(year) { + return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0); +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#number-of-days-in-month-month-of-year-year +const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; +function numberOfDaysInMonthOfYear(month, year) { + if (month === 2 && isLeapYear(year)) { + return 29; + } + return daysInMonth[month - 1]; +} + +const monthRe = /^([0-9]{4,})-([0-9]{2})$/; + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-month-string +function parseMonthString(str) { + const matches = monthRe.exec(str); + if (!matches) { + return null; + } + const year = Number(matches[1]); + if (year <= 0) { + return null; + } + const month = Number(matches[2]); + if (month < 1 || month > 12) { + return null; + } + return { year, month }; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-month-string +function isValidMonthString(str) { + return parseMonthString(str) !== null; +} +function serializeMonth({ year, month }) { + const yearStr = `${year}`.padStart(4, "0"); + const monthStr = `${month}`.padStart(2, "0"); + return `${yearStr}-${monthStr}`; +} + +const dateRe = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})$/; + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-date-string +function parseDateString(str) { + const matches = dateRe.exec(str); + if (!matches) { + return null; + } + const year = Number(matches[1]); + if (year <= 0) { + return null; + } + const month = Number(matches[2]); + if (month < 1 || month > 12) { + return null; + } + const day = Number(matches[3]); + if (day < 1 || day > numberOfDaysInMonthOfYear(month, year)) { + return null; + } + return { year, month, day }; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string +function isValidDateString(str) { + return parseDateString(str) !== null; +} +function serializeDate(date) { + const dayStr = `${date.day}`.padStart(2, "0"); + return `${serializeMonth(date)}-${dayStr}`; +} + +const yearlessDateRe = /^(?:--)?([0-9]{2})-([0-9]{2})$/; + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-yearless-date-string +function parseYearlessDateString(str) { + const matches = yearlessDateRe.exec(str); + if (!matches) { + return null; + } + const month = Number(matches[1]); + if (month < 1 || month > 12) { + return null; + } + const day = Number(matches[2]); + if (day < 1 || day > numberOfDaysInMonthOfYear(month, 4)) { + return null; + } + return { month, day }; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-yearless-date-string +function isValidYearlessDateString(str) { + return parseYearlessDateString(str) !== null; +} +function serializeYearlessDate({ month, day }) { + const monthStr = `${month}`.padStart(2, "0"); + const dayStr = `${day}`.padStart(2, "0"); + return `${monthStr}-${dayStr}`; +} + +const timeRe = /^([0-9]{2}):([0-9]{2})(?::([0-9]{2}(?:\.([0-9]{1,3}))?))?$/; + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-time-string +function parseTimeString(str) { + const matches = timeRe.exec(str); + if (!matches) { + return null; + } + const hour = Number(matches[1]); + if (hour < 0 || hour > 23) { + return null; + } + const minute = Number(matches[2]); + if (minute < 0 || minute > 59) { + return null; + } + const second = matches[3] !== undefined ? Math.trunc(Number(matches[3])) : 0; + if (second < 0 || second >= 60) { + return null; + } + const millisecond = matches[4] !== undefined ? Number(matches[4]) : 0; + return { hour, minute, second, millisecond }; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-time-string +function isValidTimeString(str) { + return parseTimeString(str) !== null; +} + +function serializeTime({ hour, minute, second, millisecond }) { + const hourStr = `${hour}`.padStart(2, "0"); + const minuteStr = `${minute}`.padStart(2, "0"); + if (second === 0 && millisecond === 0) { + return `${hourStr}:${minuteStr}`; + } + const secondStr = `${second}`.padStart(2, "0"); + const millisecondStr = `${millisecond}`.padStart(3, "0"); + return `${hourStr}:${minuteStr}:${secondStr}.${millisecondStr}`; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-local-date-and-time-string +function parseLocalDateAndTimeString(str, normalized = false) { + let separatorIdx = str.indexOf("T"); + if (separatorIdx < 0 && !normalized) { + separatorIdx = str.indexOf(" "); + } + if (separatorIdx < 0) { + return null; + } + const date = parseDateString(str.slice(0, separatorIdx)); + if (date === null) { + return null; + } + const time = parseTimeString(str.slice(separatorIdx + 1)); + if (time === null) { + return null; + } + return { date, time }; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string +function isValidLocalDateAndTimeString(str) { + return parseLocalDateAndTimeString(str) !== null; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-normalised-local-date-and-time-string +function isValidNormalizedLocalDateAndTimeString(str) { + return parseLocalDateAndTimeString(str, true) !== null; +} +function serializeNormalizedDateAndTime({ date, time }) { + return `${serializeDate(date)}T${serializeTime(time)}`; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#week-number-of-the-last-day +// https://stackoverflow.com/a/18538272/1937836 +function weekNumberOfLastDay(year) { + const jan1 = new Date(year, 0); + return jan1.getDay() === 4 || (isLeapYear(year) && jan1.getDay() === 3) ? 53 : 52; +} + +const weekRe = /^([0-9]{4,5})-W([0-9]{2})$/; + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-week-string +function parseWeekString(str) { + const matches = weekRe.exec(str); + if (!matches) { + return null; + } + const year = Number(matches[1]); + if (year <= 0) { + return null; + } + const week = Number(matches[2]); + if (week < 1 || week > weekNumberOfLastDay(year)) { + return null; + } + return { year, week }; +} + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-week-string +function isValidWeekString(str) { + return parseWeekString(str) !== null; +} +function serializeWeek({ year, week }) { + const yearStr = `${year}`.padStart(4, "0"); + const weekStr = `${week}`.padStart(2, "0"); + return `${yearStr}-W${weekStr}`; +} + +// https://stackoverflow.com/a/6117889 +function parseDateAsWeek(originalDate) { + const dayInSeconds = 86400000; + // Copy date so don't modify original + const date = new Date(Date.UTC(originalDate.getUTCFullYear(), originalDate.getUTCMonth(), originalDate.getUTCDate())); + // Set to nearest Thursday: current date + 4 - current day number + // Make Sunday's day number 7 + date.setUTCDate(date.getUTCDate() + 4 - (date.getUTCDay() || 7)); + // Get first day of year + const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); + // Calculate full weeks to nearest Thursday + const week = Math.ceil((((date - yearStart) / dayInSeconds) + 1) / 7); + + return { year: date.getUTCFullYear(), week }; +} + +function isDate(obj) { + try { + Date.prototype.valueOf.call(obj); + return true; + } catch { + return false; + } +} + +module.exports = { + isDate, + numberOfDaysInMonthOfYear, + + parseMonthString, + isValidMonthString, + serializeMonth, + + parseDateString, + isValidDateString, + serializeDate, + + parseYearlessDateString, + isValidYearlessDateString, + serializeYearlessDate, + + parseTimeString, + isValidTimeString, + serializeTime, + + parseLocalDateAndTimeString, + isValidLocalDateAndTimeString, + isValidNormalizedLocalDateAndTimeString, + serializeNormalizedDateAndTime, + + parseDateAsWeek, + weekNumberOfLastDay, + parseWeekString, + isValidWeekString, + serializeWeek +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/details.js b/node_modules/jsdom/lib/jsdom/living/helpers/details.js new file mode 100644 index 0000000..25c5387 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/details.js @@ -0,0 +1,15 @@ +"use strict"; +const { firstChildWithLocalName } = require("./traversal"); +const { HTML_NS } = require("./namespaces"); + +// https://html.spec.whatwg.org/multipage/interactive-elements.html#summary-for-its-parent-details +exports.isSummaryForParentDetails = summaryElement => { + const parent = summaryElement.parentNode; + if (parent === null) { + return false; + } + if (parent._localName !== "details" || parent._namespaceURI !== HTML_NS) { + return false; + } + return firstChildWithLocalName(parent, "summary") === summaryElement; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/document-base-url.js b/node_modules/jsdom/lib/jsdom/living/helpers/document-base-url.js new file mode 100644 index 0000000..f69e061 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/document-base-url.js @@ -0,0 +1,54 @@ +"use strict"; +const whatwgURL = require("whatwg-url"); +const { implForWrapper } = require("../generated/utils"); + +exports.documentBaseURL = document => { + // https://html.spec.whatwg.org/multipage/infrastructure.html#document-base-url + + const firstBase = document.querySelector("base[href]"); + const fallbackBaseURL = exports.fallbackBaseURL(document); + + if (firstBase === null) { + return fallbackBaseURL; + } + + return frozenBaseURL(firstBase, fallbackBaseURL); +}; + +exports.documentBaseURLSerialized = document => { + return whatwgURL.serializeURL(exports.documentBaseURL(document)); +}; + +exports.fallbackBaseURL = document => { + // https://html.spec.whatwg.org/multipage/infrastructure.html#fallback-base-url + + // Unimplemented: <iframe srcdoc> + + if (document.URL === "about:blank" && document._defaultView && + document._defaultView._parent !== document._defaultView) { + const parentDocument = implForWrapper(document._defaultView._parent._document); + return exports.documentBaseURL(parentDocument); + } + + return document._URL; +}; + +exports.parseURLToResultingURLRecord = (url, document) => { + // https://html.spec.whatwg.org/#resolve-a-url + + // Encoding stuff ignored; always UTF-8 for us, for now. + + const baseURL = exports.documentBaseURL(document); + + return whatwgURL.parseURL(url, { baseURL }); + // This returns the resulting URL record; to get the resulting URL string, just serialize it. +}; + +function frozenBaseURL(baseElement, fallbackBaseURL) { + // https://html.spec.whatwg.org/multipage/semantics.html#frozen-base-url + // The spec is eager (setting the frozen base URL when things change); we are lazy (getting it when we need to) + + const baseHrefAttribute = baseElement.getAttributeNS(null, "href"); + const result = whatwgURL.parseURL(baseHrefAttribute, { baseURL: fallbackBaseURL }); + return result === null ? fallbackBaseURL : result; +} diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/events.js b/node_modules/jsdom/lib/jsdom/living/helpers/events.js new file mode 100644 index 0000000..cd65a38 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/events.js @@ -0,0 +1,24 @@ +"use strict"; + +const Event = require("../generated/Event"); +const { tryImplForWrapper } = require("../generated/utils"); + +function createAnEvent(e, globalObject, eventInterface = Event, attributes = {}) { + return eventInterface.createImpl( + globalObject, + [e, attributes], + { isTrusted: attributes.isTrusted !== false } + ); +} + +function fireAnEvent(e, target, eventInterface, attributes, legacyTargetOverrideFlag) { + const event = createAnEvent(e, target._globalObject, eventInterface, attributes); + + // tryImplForWrapper() is currently required due to use in Window.js + return tryImplForWrapper(target)._dispatch(event, legacyTargetOverrideFlag); +} + +module.exports = { + createAnEvent, + fireAnEvent +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/focusing.js b/node_modules/jsdom/lib/jsdom/living/helpers/focusing.js new file mode 100644 index 0000000..7d1a38c --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/focusing.js @@ -0,0 +1,104 @@ +"use strict"; +const FocusEvent = require("../generated/FocusEvent.js"); +const idlUtils = require("../generated/utils.js"); +const { isDisabled } = require("./form-controls.js"); +const { firstChildWithLocalName } = require("./traversal"); +const { createAnEvent } = require("./events"); +const { HTML_NS, SVG_NS } = require("./namespaces"); +const { isRenderedElement } = require("./svg/render"); + +const focusableFormElements = new Set(["input", "select", "textarea", "button"]); + +// https://html.spec.whatwg.org/multipage/interaction.html#focusable-area, but also some of +// https://html.spec.whatwg.org/multipage/interaction.html#focusing-steps and some of +// https://svgwg.org/svg2-draft/interact.html#TermFocusable +exports.isFocusableAreaElement = elImpl => { + // We implemented most of the suggested focusable elements found here: + // https://html.spec.whatwg.org/multipage/interaction.html#tabindex-value + // However, some suggested elements are not focusable in web browsers, as detailed here: + // https://github.com/whatwg/html/issues/5490 + if (elImpl._namespaceURI === HTML_NS) { + if (!elImpl._ownerDocument._defaultView) { + return false; + } + + if (!elImpl.isConnected) { + return false; + } + + if (!Number.isNaN(parseInt(elImpl.getAttributeNS(null, "tabindex")))) { + return true; + } + + if (elImpl._localName === "iframe") { + return true; + } + + if (elImpl._localName === "a" && elImpl.hasAttributeNS(null, "href")) { + return true; + } + + if (elImpl._localName === "summary" && elImpl.parentNode && + elImpl.parentNode._localName === "details" && + elImpl === firstChildWithLocalName(elImpl.parentNode, "summary")) { + return true; + } + + if (focusableFormElements.has(elImpl._localName) && !isDisabled(elImpl)) { + if (elImpl._localName === "input" && elImpl.type === "hidden") { + return false; + } + + return true; + } + + if (elImpl.hasAttributeNS(null, "contenteditable")) { + return true; + } + + return false; + + // This does not check for a designMode Document as specified in + // https://html.spec.whatwg.org/multipage/interaction.html#editing-host because the designMode + // attribute is not implemented. + } + + if (elImpl._namespaceURI === SVG_NS) { + if (!Number.isNaN(parseInt(elImpl.getAttributeNS(null, "tabindex"))) && isRenderedElement(elImpl)) { + return true; + } + + if (elImpl._localName === "a" && elImpl.hasAttributeNS(null, "href")) { + return true; + } + + return false; + } + + return false; +}; + +// https://html.spec.whatwg.org/multipage/interaction.html#fire-a-focus-event plus the steps of +// https://html.spec.whatwg.org/multipage/interaction.html#focus-update-steps that adjust Documents to Windows +// It's extended with the bubbles option to also handle focusin/focusout, which are "defined" in +// https://w3c.github.io/uievents/#event-type-focusin. See https://github.com/whatwg/html/issues/3514. +exports.fireFocusEventWithTargetAdjustment = (name, target, relatedTarget, { bubbles = false } = {}) => { + if (target === null) { + // E.g. firing blur with nothing previously focused. + return; + } + + const event = createAnEvent(name, target._globalObject, FocusEvent, { + bubbles, + composed: true, + relatedTarget, + view: target._ownerDocument._defaultView, + detail: 0 + }); + + if (target._defaultView) { + target = idlUtils.implForWrapper(target._defaultView); + } + + target._dispatch(event); +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/form-controls.js b/node_modules/jsdom/lib/jsdom/living/helpers/form-controls.js new file mode 100644 index 0000000..bd4a799 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/form-controls.js @@ -0,0 +1,306 @@ +"use strict"; + +const { + isValidFloatingPointNumber, + isValidSimpleColor, + parseFloatingPointNumber, + stripLeadingAndTrailingASCIIWhitespace, + stripNewlines, + splitOnCommas +} = require("./strings"); +const { + isValidDateString, + isValidMonthString, + isValidTimeString, + isValidWeekString, + parseLocalDateAndTimeString, + serializeNormalizedDateAndTime +} = require("./dates-and-times"); +const whatwgURL = require("whatwg-url"); + +const NodeList = require("../generated/NodeList"); +const { domSymbolTree } = require("./internal-constants"); +const { closest, firstChildWithLocalName } = require("./traversal"); +const NODE_TYPE = require("../node-type"); +const { HTML_NS } = require("./namespaces"); + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled +exports.isDisabled = formControl => { + if (formControl.localName === "button" || formControl.localName === "input" || formControl.localName === "select" || + formControl.localName === "textarea") { + if (formControl.hasAttributeNS(null, "disabled")) { + return true; + } + } + + let e = formControl.parentNode; + while (e) { + if (e.localName === "fieldset" && e.hasAttributeNS(null, "disabled")) { + const firstLegendElementChild = firstChildWithLocalName(e, "legend"); + if (!firstLegendElementChild || !firstLegendElementChild.contains(formControl)) { + return true; + } + } + e = e.parentNode; + } + + return false; +}; + +// https://html.spec.whatwg.org/multipage/forms.html#category-listed +const listedElements = new Set(["button", "fieldset", "input", "object", "output", "select", "textarea"]); +exports.isListed = formControl => listedElements.has(formControl._localName) && formControl.namespaceURI === HTML_NS; + +// https://html.spec.whatwg.org/multipage/forms.html#category-submit +const submittableElements = new Set(["button", "input", "object", "select", "textarea"]); +exports.isSubmittable = formControl => { + return submittableElements.has(formControl._localName) && formControl.namespaceURI === HTML_NS; +}; + +// https://html.spec.whatwg.org/multipage/forms.html#concept-submit-button +const submitButtonInputTypes = new Set(["submit", "image"]); +exports.isSubmitButton = formControl => { + return ((formControl._localName === "input" && submitButtonInputTypes.has(formControl.type)) || + (formControl._localName === "button" && formControl.type === "submit")) && + formControl.namespaceURI === HTML_NS; +}; + +// https://html.spec.whatwg.org/multipage/forms.html#concept-button +const buttonInputTypes = new Set([...submitButtonInputTypes, "reset", "button"]); +exports.isButton = formControl => { + return ((formControl._localName === "input" && buttonInputTypes.has(formControl.type)) || + formControl._localName === "button") && + formControl.namespaceURI === HTML_NS; +}; + +// https://html.spec.whatwg.org/multipage/dom.html#interactive-content-2 +exports.isInteractiveContent = node => { + if (node.nodeType !== NODE_TYPE.ELEMENT_NODE) { + return false; + } + if (node.namespaceURI !== HTML_NS) { + return false; + } + if (node.hasAttributeNS(null, "tabindex")) { + return true; + } + switch (node.localName) { + case "a": + return node.hasAttributeNS(null, "href"); + + case "audio": + case "video": + return node.hasAttributeNS(null, "controls"); + + case "img": + case "object": + return node.hasAttributeNS(null, "usemap"); + + case "input": + return node.type !== "hidden"; + + case "button": + case "details": + case "embed": + case "iframe": + case "label": + case "select": + case "textarea": + return true; + } + + return false; +}; + +// https://html.spec.whatwg.org/multipage/forms.html#category-label +exports.isLabelable = node => { + if (node.nodeType !== NODE_TYPE.ELEMENT_NODE) { + return false; + } + if (node.namespaceURI !== HTML_NS) { + return false; + } + switch (node.localName) { + case "button": + case "meter": + case "output": + case "progress": + case "select": + case "textarea": + return true; + + case "input": + return node.type !== "hidden"; + } + + return false; +}; + +exports.getLabelsForLabelable = labelable => { + if (!exports.isLabelable(labelable)) { + return null; + } + if (!labelable._labels) { + const root = labelable.getRootNode({}); + labelable._labels = NodeList.create(root._globalObject, [], { + element: root, + query: () => { + const nodes = []; + for (const descendant of domSymbolTree.treeIterator(root)) { + if (descendant.control === labelable) { + nodes.push(descendant); + } + } + return nodes; + } + }); + } + return labelable._labels; +}; + +// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address +exports.isValidEmailAddress = (emailAddress, multiple = false) => { + const emailAddressRegExp = new RegExp("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9]" + + "(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}" + + "[a-zA-Z0-9])?)*$"); + // A valid e-mail address list is a set of comma-separated tokens, where each token is itself + // a valid e - mail address.To obtain the list of tokens from a valid e - mail address list, + // an implementation must split the string on commas. + if (multiple) { + return splitOnCommas(emailAddress).every(value => emailAddressRegExp.test(value)); + } + return emailAddressRegExp.test(emailAddress); +}; + +exports.isValidAbsoluteURL = url => { + return whatwgURL.parseURL(url) !== null; +}; + +exports.sanitizeValueByType = (input, val) => { + switch (input.type.toLowerCase()) { + case "password": + case "search": + case "tel": + case "text": + val = stripNewlines(val); + break; + + case "color": + // https://html.spec.whatwg.org/multipage/forms.html#color-state-(type=color):value-sanitization-algorithm + val = isValidSimpleColor(val) ? val.toLowerCase() : "#000000"; + break; + + case "date": + // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):value-sanitization-algorithm + if (!isValidDateString(val)) { + val = ""; + } + break; + + case "datetime-local": { + // https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local):value-sanitization-algorithm + const dateAndTime = parseLocalDateAndTimeString(val); + val = dateAndTime !== null ? serializeNormalizedDateAndTime(dateAndTime) : ""; + break; + } + + case "email": + // https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email):value-sanitization-algorithm + // https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email):value-sanitization-algorithm-2 + if (input.hasAttributeNS(null, "multiple")) { + val = val.split(",").map(token => stripLeadingAndTrailingASCIIWhitespace(token)).join(","); + } else { + val = stripNewlines(val); + val = stripLeadingAndTrailingASCIIWhitespace(val); + } + break; + + case "month": + // https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):value-sanitization-algorithm + if (!isValidMonthString(val)) { + val = ""; + } + break; + + case "number": + // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):value-sanitization-algorithm + // TODO: using parseFloatingPointNumber in addition to isValidFloatingPointNumber to pass number.html WPT. + // Possible spec bug. + if (!isValidFloatingPointNumber(val) || parseFloatingPointNumber(val) === null) { + val = ""; + } + break; + + case "range": + // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):value-sanitization-algorithm + // TODO: using parseFloatingPointNumber in addition to isValidFloatingPointNumber to pass number.html WPT. + // Possible spec bug. + if (!isValidFloatingPointNumber(val) || parseFloatingPointNumber(val) === null) { + const minimum = input._minimum; + const maximum = input._maximum; + const defaultValue = maximum < minimum ? minimum : (minimum + maximum) / 2; + val = `${defaultValue}`; + } else if (val < input._minimum) { + val = `${input._minimum}`; + } else if (val > input._maximum) { + val = `${input._maximum}`; + } + break; + + case "time": + // https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):value-sanitization-algorithm + if (!isValidTimeString(val)) { + val = ""; + } + break; + + case "url": + // https://html.spec.whatwg.org/multipage/forms.html#url-state-(type=url):value-sanitization-algorithm + val = stripNewlines(val); + val = stripLeadingAndTrailingASCIIWhitespace(val); + break; + + case "week": + // https://html.spec.whatwg.org/multipage/input.html#week-state-(type=week):value-sanitization-algorithm + if (!isValidWeekString(val)) { + val = ""; + } + } + + return val; +}; + +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-owner +// TODO: The spec describes an imperative process for assigning/resetting an element's form +// owner based on activities involving form-associated elements. This simpler implementation +// instead calculates the current form owner only when the property is accessed. This is not +// sufficient to pass all the web platform tests, but is good enough for most purposes. We +// should eventually update it to use the correct version, though. See +// https://github.com/whatwg/html/issues/4050 for some discussion. + +exports.formOwner = formControl => { + const formAttr = formControl.getAttributeNS(null, "form"); + if (formAttr === "") { + return null; + } + if (formAttr === null) { + return closest(formControl, "form"); + } + + const root = formControl.getRootNode({}); + let firstElementWithId; + for (const descendant of domSymbolTree.treeIterator(root)) { + if (descendant.nodeType === NODE_TYPE.ELEMENT_NODE && + descendant.getAttributeNS(null, "id") === formAttr) { + firstElementWithId = descendant; + break; + } + } + + if (firstElementWithId && + firstElementWithId.namespaceURI === HTML_NS && + firstElementWithId.localName === "form") { + return firstElementWithId; + } + return null; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/html-constructor.js b/node_modules/jsdom/lib/jsdom/living/helpers/html-constructor.js new file mode 100644 index 0000000..ffaf377 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/html-constructor.js @@ -0,0 +1,78 @@ +"use strict"; + +const { HTML_NS } = require("./namespaces"); +const { createElement, getValidTagNames } = require("./create-element"); + +const { implForWrapper, wrapperForImpl } = require("../generated/utils"); + +// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-already-constructed-marker +const ALREADY_CONSTRUCTED_MARKER = Symbol("already-constructed-marker"); + +// https://html.spec.whatwg.org/multipage/dom.html#htmlconstructor +function HTMLConstructor(globalObject, constructorName, newTarget) { + const registry = implForWrapper(globalObject.customElements); + if (newTarget === HTMLConstructor) { + throw new TypeError("Invalid constructor"); + } + + const definition = registry._customElementDefinitions.find(entry => entry.objectReference === newTarget); + if (definition === undefined) { + throw new TypeError("Invalid constructor, the constructor is not part of the custom element registry"); + } + + let isValue = null; + + if (definition.localName === definition.name) { + if (constructorName !== "HTMLElement") { + throw new TypeError("Invalid constructor, autonomous custom element should extend from HTMLElement"); + } + } else { + const validLocalNames = getValidTagNames(HTML_NS, constructorName); + if (!validLocalNames.includes(definition.localName)) { + throw new TypeError(`${definition.localName} is not valid local name for ${constructorName}`); + } + + isValue = definition.name; + } + + let { prototype } = newTarget; + + if (prototype === null || typeof prototype !== "object") { + // The following line deviates from the specification. The HTMLElement prototype should be retrieved from the realm + // associated with the "new.target". Because it is impossible to get such information in jsdom, we fallback to the + // HTMLElement prototype associated with the current object. + prototype = globalObject.HTMLElement.prototype; + } + + if (definition.constructionStack.length === 0) { + const documentImpl = implForWrapper(globalObject.document); + + const elementImpl = createElement(documentImpl, definition.localName, HTML_NS); + + const element = wrapperForImpl(elementImpl); + Object.setPrototypeOf(element, prototype); + + elementImpl._ceState = "custom"; + elementImpl._ceDefinition = definition; + elementImpl._isValue = isValue; + + return element; + } + + const elementImpl = definition.constructionStack[definition.constructionStack.length - 1]; + const element = wrapperForImpl(elementImpl); + + if (elementImpl === ALREADY_CONSTRUCTED_MARKER) { + throw new TypeError("This instance is already constructed"); + } + + Object.setPrototypeOf(element, prototype); + + definition.constructionStack[definition.constructionStack.length - 1] = ALREADY_CONSTRUCTED_MARKER; + + return element; +} + +module.exports = { + HTMLConstructor +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/http-request.js b/node_modules/jsdom/lib/jsdom/living/helpers/http-request.js new file mode 100644 index 0000000..616a806 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/http-request.js @@ -0,0 +1,254 @@ +"use strict"; +const http = require("http"); +const https = require("https"); +const { Writable } = require("stream"); +const zlib = require("zlib"); + +const ver = process.version.replace("v", "").split("."); +const majorNodeVersion = Number.parseInt(ver[0]); + +function abortRequest(clientRequest) { + // clientRequest.destroy breaks the test suite for versions 10 and 12, + // hence the version check + if (majorNodeVersion > 13) { + clientRequest.destroy(); + } else { + clientRequest.abort(); + } + clientRequest.removeAllListeners(); + clientRequest.on("error", () => {}); +} + +module.exports = class Request extends Writable { + constructor(url, clientOptions, requestOptions) { + super(); + Object.assign(this, clientOptions); + this.currentURL = url; + this._requestOptions = requestOptions; + this.headers = requestOptions.headers; + this._ended = false; + this._redirectCount = 0; + this._requestBodyBuffers = []; + this._bufferIndex = 0; + this._performRequest(); + } + + abort() { + abortRequest(this._currentRequest); + this.emit("abort"); + this.removeAllListeners(); + } + + pipeRequest(form) { + form.pipe(this._currentRequest); + } + + write(data, encoding) { + if (data.length > 0) { + this._requestBodyBuffers.push({ data, encoding }); + this._currentRequest.write(data, encoding); + } + } + + end() { + this.emit("request", this._currentRequest); + this._ended = true; + this._currentRequest.end(); + } + + setHeader(name, value) { + this.headers[name] = value; + this._currentRequest.setHeader(name, value); + } + + removeHeader(name) { + delete this.headers[name]; + this._currentRequest.removeHeader(name); + } + + // Without this method, the test send-redirect-infinite-sync will halt the test suite + // TODO: investigate this further and ideally remove + toJSON() { + const { method, headers } = this._requestOptions; + return { uri: new URL(this.currentURL), method, headers }; + } + + _writeNext(error) { + if (this._currentRequest) { + if (error) { + this.emit("error", error); + } else if (this._bufferIndex < this._requestBodyBuffers.length) { + const buffer = this._requestBodyBuffers[this._bufferIndex++]; + if (!this._currentRequest.writableEnded) { + this._currentRequest.write( + buffer.data, + buffer.encoding, + this._writeNext.bind(this) + ); + } + } else if (this._ended) { + this._currentRequest.end(); + } + } + } + + _performRequest() { + const urlOptions = new URL(this.currentURL); + const scheme = urlOptions.protocol; + this._requestOptions.agent = this.agents[scheme.substring(0, scheme.length - 1)]; + const { request } = scheme === "https:" ? https : http; + this._currentRequest = request(this.currentURL, this._requestOptions, response => { + this._processResponse(response); + }); + + let cookies; + if (this._redirectCount === 0) { + this.originalCookieHeader = this.getHeader("Cookie"); + } + if (this.cookieJar) { + cookies = this.cookieJar.getCookieStringSync(this.currentURL); + } + if (cookies && cookies.length) { + if (this.originalCookieHeader) { + this.setHeader("Cookie", this.originalCookieHeader + "; " + cookies); + } else { + this.setHeader("Cookie", cookies); + } + } + + for (const event of ["connect", "error", "socket", "timeout"]) { + this._currentRequest.on(event, (...args) => { + this.emit(event, ...args); + }); + } + if (this._isRedirect) { + this._bufferIndex = 0; + this._writeNext(); + } + } + + _processResponse(response) { + const cookies = response.headers["set-cookie"]; + if (this.cookieJar && Array.isArray(cookies)) { + try { + cookies.forEach(cookie => { + this.cookieJar.setCookieSync(cookie, this.currentURL, { ignoreError: true }); + }); + } catch (e) { + this.emit("error", e); + } + } + + const { statusCode } = response; + const { location } = response.headers; + // In Node v15, aborting a message with remaining data causes an error to be thrown, + // hence the version check + const catchResErrors = err => { + if (!(majorNodeVersion >= 15 && err.message === "aborted")) { + this.emit("error", err); + } + }; + response.on("error", catchResErrors); + let redirectAddress = null; + let resendWithAuth = false; + if (typeof location === "string" && + location.length && + this.followRedirects && + statusCode >= 300 && + statusCode < 400) { + redirectAddress = location; + } else if (statusCode === 401 && + /^Basic /i.test(response.headers["www-authenticate"] || "") && + (this.user && this.user.length)) { + this._requestOptions.auth = `${this.user}:${this.pass}`; + resendWithAuth = true; + } + if (redirectAddress || resendWithAuth) { + if (++this._redirectCount > 21) { + const redirectError = new Error("Maximum number of redirects exceeded"); + redirectError.code = "ERR_TOO_MANY_REDIRECTS"; + this.emit("error", redirectError); + return; + } + abortRequest(this._currentRequest); + response.destroy(); + this._isRedirect = true; + if (((statusCode === 301 || statusCode === 302) && this._requestOptions.method === "POST") || + (statusCode === 303 && !/^(?:GET|HEAD)$/.test(this._requestOptions.method))) { + this._requestOptions.method = "GET"; + this._requestBodyBuffers = []; + } + let previousHostName = this._removeMatchingHeaders(/^host$/i); + if (!previousHostName) { + previousHostName = new URL(this.currentURL).hostname; + } + const previousURL = this.currentURL; + if (!resendWithAuth) { + const nextURL = redirectAddress.startsWith("https:") ? + new URL(redirectAddress) : + new URL(redirectAddress, this.currentURL); + if (nextURL.hostname !== previousHostName) { + this._removeMatchingHeaders(/^authorization$/i); + } + this.currentURL = nextURL.toString(); + } + this.headers.Referer = previousURL; + this.emit("redirect", response, this.headers, this.currentURL); + try { + this._performRequest(); + } catch (cause) { + this.emit("error", cause); + } + } else { + let pipeline = response; + const acceptEncoding = this.headers["Accept-Encoding"]; + const requestCompressed = typeof acceptEncoding === "string" && + (acceptEncoding.includes("gzip") || acceptEncoding.includes("deflate")); + if ( + requestCompressed && + this._requestOptions.method !== "HEAD" && + statusCode >= 200 && + statusCode !== 204 && + statusCode !== 304 + ) { + const zlibOptions = { + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + }; + const contentEncoding = (response.headers["content-encoding"] || "identity").trim().toLowerCase(); + if (contentEncoding === "gzip") { + pipeline = zlib.createGunzip(zlibOptions); + response.pipe(pipeline); + } else if (contentEncoding === "deflate") { + pipeline = zlib.createInflate(zlibOptions); + response.pipe(pipeline); + } + } + pipeline.removeAllListeners("error"); + this.emit("response", response, this.currentURL); + pipeline.on("data", bytes => this.emit("data", bytes)); + pipeline.once("end", bytes => this.emit("end", bytes)); + pipeline.on("error", catchResErrors); + pipeline.on("close", () => this.emit("close")); + this._requestBodyBuffers = []; + } + } + + getHeader(key, value) { + if (this._currentRequest) { + return this._currentRequest.getHeader(key, value); + } + return null; + } + + _removeMatchingHeaders(regex) { + let lastValue; + for (const header in this.headers) { + if (regex.test(header)) { + lastValue = this.headers[header]; + delete this.headers[header]; + } + } + return lastValue; + } +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/internal-constants.js b/node_modules/jsdom/lib/jsdom/living/helpers/internal-constants.js new file mode 100644 index 0000000..707add9 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/internal-constants.js @@ -0,0 +1,12 @@ +"use strict"; +const SymbolTree = require("symbol-tree"); + +exports.cloningSteps = Symbol("cloning steps"); + +// TODO: the many underscore-prefixed hooks should move here +// E.g. _attrModified (which maybe should be split into its per-spec variants) + +/** + * This SymbolTree is used to build the tree for all Node in a document + */ +exports.domSymbolTree = new SymbolTree("DOM SymbolTree"); diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/iterable-weak-set.js b/node_modules/jsdom/lib/jsdom/living/helpers/iterable-weak-set.js new file mode 100644 index 0000000..9d5e167 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/iterable-weak-set.js @@ -0,0 +1,48 @@ +"use strict"; + +// An iterable WeakSet implementation inspired by the iterable WeakMap example code in the WeakRefs specification: +// https://github.com/tc39/proposal-weakrefs#iterable-weakmaps +module.exports = class IterableWeakSet { + constructor() { + this._refSet = new Set(); + this._refMap = new WeakMap(); + this._finalizationRegistry = new FinalizationRegistry(({ ref, set }) => set.delete(ref)); + } + + add(value) { + if (!this._refMap.has(value)) { + const ref = new WeakRef(value); + this._refMap.set(value, ref); + this._refSet.add(ref); + this._finalizationRegistry.register(value, { ref, set: this._refSet }, ref); + } + + return this; + } + + delete(value) { + const ref = this._refMap.get(value); + if (!ref) { + return false; + } + + this._refMap.delete(value); + this._refSet.delete(ref); + this._finalizationRegistry.unregister(ref); + return true; + } + + has(value) { + return this._refMap.has(value); + } + + * [Symbol.iterator]() { + for (const ref of this._refSet) { + const value = ref.deref(); + if (value === undefined) { + continue; + } + yield value; + } + } +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/json.js b/node_modules/jsdom/lib/jsdom/living/helpers/json.js new file mode 100644 index 0000000..6920bc2 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/json.js @@ -0,0 +1,12 @@ +"use strict"; + +// https://infra.spec.whatwg.org/#parse-json-from-bytes +exports.parseJSONFromBytes = bytes => { + // https://encoding.spec.whatwg.org/#utf-8-decode + if (bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) { + bytes = bytes.subarray(3); + } + const jsonText = bytes.toString("utf-8"); + + return JSON.parse(jsonText); +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/mutation-observers.js b/node_modules/jsdom/lib/jsdom/living/helpers/mutation-observers.js new file mode 100644 index 0000000..c1c9209 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/mutation-observers.js @@ -0,0 +1,198 @@ +"use strict"; + +const { domSymbolTree } = require("./internal-constants"); +const reportException = require("./runtime-script-errors"); + +const Event = require("../generated/Event"); +const idlUtils = require("../generated/utils"); +const MutationRecord = require("../generated/MutationRecord"); + +const MUTATION_TYPE = { + ATTRIBUTES: "attributes", + CHARACTER_DATA: "characterData", + CHILD_LIST: "childList" +}; + +// Note: +// Since jsdom doesn't currently implement the concept of "unit of related similar-origin browsing contexts" +// (https://html.spec.whatwg.org/multipage/browsers.html#unit-of-related-similar-origin-browsing-contexts) +// we will approximate that the following properties are global for now. + +// https://dom.spec.whatwg.org/#mutation-observer-compound-microtask-queued-flag +let mutationObserverMicrotaskQueueFlag = false; + +// Non-spec compliant: List of all the mutation observers with mutation records enqueued. It's a replacement for +// mutation observer list (https://dom.spec.whatwg.org/#mutation-observer-list) but without leaking since it's empty +// before notifying the mutation observers. +const activeMutationObservers = new Set(); + +// https://dom.spec.whatwg.org/#signal-slot-list +const signalSlotList = []; + +// https://dom.spec.whatwg.org/#queue-a-mutation-record +function queueMutationRecord( + type, + target, + name, + namespace, + oldValue, + addedNodes, + removedNodes, + previousSibling, + nextSibling +) { + const interestedObservers = new Map(); + + const nodes = domSymbolTree.ancestorsToArray(target); + + for (const node of nodes) { + for (const registered of node._registeredObserverList) { + const { options, observer: mo } = registered; + + if ( + !(node !== target && options.subtree === false) && + !(type === MUTATION_TYPE.ATTRIBUTES && options.attributes !== true) && + !(type === MUTATION_TYPE.ATTRIBUTES && options.attributeFilter && + !options.attributeFilter.some(value => value === name || value === namespace)) && + !(type === MUTATION_TYPE.CHARACTER_DATA && options.characterData !== true) && + !(type === MUTATION_TYPE.CHILD_LIST && options.childList === false) + ) { + if (!interestedObservers.has(mo)) { + interestedObservers.set(mo, null); + } + + if ( + (type === MUTATION_TYPE.ATTRIBUTES && options.attributeOldValue === true) || + (type === MUTATION_TYPE.CHARACTER_DATA && options.characterDataOldValue === true) + ) { + interestedObservers.set(mo, oldValue); + } + } + } + } + + for (const [observer, mappedOldValue] of interestedObservers.entries()) { + const record = MutationRecord.createImpl(target._globalObject, [], { + type, + target, + attributeName: name, + attributeNamespace: namespace, + oldValue: mappedOldValue, + addedNodes, + removedNodes, + previousSibling, + nextSibling + }); + + observer._recordQueue.push(record); + activeMutationObservers.add(observer); + } + + queueMutationObserverMicrotask(); +} + +// https://dom.spec.whatwg.org/#queue-a-tree-mutation-record +function queueTreeMutationRecord(target, addedNodes, removedNodes, previousSibling, nextSibling) { + queueMutationRecord( + MUTATION_TYPE.CHILD_LIST, + target, + null, + null, + null, + addedNodes, + removedNodes, + previousSibling, + nextSibling + ); +} + +// https://dom.spec.whatwg.org/#queue-an-attribute-mutation-record +function queueAttributeMutationRecord(target, name, namespace, oldValue) { + queueMutationRecord( + MUTATION_TYPE.ATTRIBUTES, + target, + name, + namespace, + oldValue, + [], + [], + null, + null + ); +} + +// https://dom.spec.whatwg.org/#queue-a-mutation-observer-compound-microtask +function queueMutationObserverMicrotask() { + if (mutationObserverMicrotaskQueueFlag) { + return; + } + + mutationObserverMicrotaskQueueFlag = true; + + Promise.resolve().then(() => { + notifyMutationObservers(); + }); +} + +// https://dom.spec.whatwg.org/#notify-mutation-observers +function notifyMutationObservers() { + mutationObserverMicrotaskQueueFlag = false; + + const notifyList = [...activeMutationObservers].sort((a, b) => a._id - b._id); + activeMutationObservers.clear(); + + const signalList = [...signalSlotList]; + signalSlotList.splice(0, signalSlotList.length); + + for (const mo of notifyList) { + const records = [...mo._recordQueue]; + mo._recordQueue = []; + + for (const node of mo._nodeList) { + node._registeredObserverList = node._registeredObserverList.filter(registeredObserver => { + return registeredObserver.source !== mo; + }); + } + + if (records.length > 0) { + try { + const moWrapper = idlUtils.wrapperForImpl(mo); + mo._callback.call( + moWrapper, + records.map(idlUtils.wrapperForImpl), + moWrapper + ); + } catch (e) { + const { target } = records[0]; + const window = target._ownerDocument._defaultView; + + reportException(window, e); + } + } + } + + for (const slot of signalList) { + const slotChangeEvent = Event.createImpl( + slot._globalObject, + [ + "slotchange", + { bubbles: true } + ], + { isTrusted: true } + ); + + slot._dispatch(slotChangeEvent); + } +} + +module.exports = { + MUTATION_TYPE, + + queueMutationRecord, + queueTreeMutationRecord, + queueAttributeMutationRecord, + + queueMutationObserverMicrotask, + + signalSlotList +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/namespaces.js b/node_modules/jsdom/lib/jsdom/living/helpers/namespaces.js new file mode 100644 index 0000000..ec8eccc --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/namespaces.js @@ -0,0 +1,15 @@ +"use strict"; + +// https://infra.spec.whatwg.org/#namespaces + +exports.HTML_NS = "http://www.w3.org/1999/xhtml"; + +exports.MATHML_NS = "http://www.w3.org/1998/Math/MathML"; + +exports.SVG_NS = "http://www.w3.org/2000/svg"; + +exports.XLINK_NS = "http://www.w3.org/1999/xlink"; + +exports.XML_NS = "http://www.w3.org/XML/1998/namespace"; + +exports.XMLNS_NS = "http://www.w3.org/2000/xmlns/"; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/node.js b/node_modules/jsdom/lib/jsdom/living/helpers/node.js new file mode 100644 index 0000000..40e12bc --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/node.js @@ -0,0 +1,68 @@ +"use strict"; + +const NODE_TYPE = require("../node-type"); +const { domSymbolTree } = require("./internal-constants"); + +// https://dom.spec.whatwg.org/#concept-node-length +function nodeLength(node) { + switch (node.nodeType) { + case NODE_TYPE.DOCUMENT_TYPE_NODE: + return 0; + + case NODE_TYPE.TEXT_NODE: + case NODE_TYPE.PROCESSING_INSTRUCTION_NODE: + case NODE_TYPE.COMMENT_NODE: + return node.data.length; + + default: + return domSymbolTree.childrenCount(node); + } +} + +// https://dom.spec.whatwg.org/#concept-tree-root +function nodeRoot(node) { + while (domSymbolTree.parent(node)) { + node = domSymbolTree.parent(node); + } + + return node; +} + +// https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor +function isInclusiveAncestor(ancestorNode, node) { + while (node) { + if (ancestorNode === node) { + return true; + } + + node = domSymbolTree.parent(node); + } + + return false; +} + +// https://dom.spec.whatwg.org/#concept-tree-following +function isFollowing(nodeA, nodeB) { + if (nodeA === nodeB) { + return false; + } + + let current = nodeB; + while (current) { + if (current === nodeA) { + return true; + } + + current = domSymbolTree.following(current); + } + + return false; +} + +module.exports = { + nodeLength, + nodeRoot, + + isInclusiveAncestor, + isFollowing +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/number-and-date-inputs.js b/node_modules/jsdom/lib/jsdom/living/helpers/number-and-date-inputs.js new file mode 100644 index 0000000..e29bc74 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/number-and-date-inputs.js @@ -0,0 +1,195 @@ +"use strict"; +const { parseFloatingPointNumber } = require("./strings"); +const { + parseDateString, + parseLocalDateAndTimeString, + parseMonthString, + parseTimeString, + parseWeekString, + + serializeDate, + serializeMonth, + serializeNormalizedDateAndTime, + serializeTime, + serializeWeek, + parseDateAsWeek +} = require("./dates-and-times"); + +// Necessary because Date.UTC() treats year within [0, 99] as [1900, 1999]. +function getUTCMs(year, month = 1, day = 1, hour = 0, minute = 0, second = 0, millisecond = 0) { + if (year > 99 || year < 0) { + return Date.UTC(year, month - 1, day, hour, minute, second, millisecond); + } + const d = new Date(0); + d.setUTCFullYear(year); + d.setUTCMonth(month - 1); + d.setUTCDate(day); + d.setUTCHours(hour); + d.setUTCMinutes(minute); + d.setUTCSeconds(second, millisecond); + return d.valueOf(); +} + +const dayOfWeekRelMondayLUT = [-1, 0, 1, 2, 3, -3, -2]; + +exports.convertStringToNumberByType = { + // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):concept-input-value-string-number + date(input) { + const date = parseDateString(input); + if (date === null) { + return null; + } + return getUTCMs(date.year, date.month, date.day); + }, + // https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):concept-input-value-string-number + month(input) { + const date = parseMonthString(input); + if (date === null) { + return null; + } + return (date.year - 1970) * 12 + (date.month - 1); + }, + // https://html.spec.whatwg.org/multipage/input.html#week-state-(type=week):concept-input-value-string-number + week(input) { + const date = parseWeekString(input); + if (date === null) { + return null; + } + const dateObj = new Date(getUTCMs(date.year)); + // An HTML week starts on Monday, while 0 represents Sunday. Account for such. + const dayOfWeekRelMonday = dayOfWeekRelMondayLUT[dateObj.getUTCDay()]; + return dateObj.setUTCDate(1 + 7 * (date.week - 1) - dayOfWeekRelMonday); + }, + // https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):concept-input-value-string-number + time(input) { + const time = parseTimeString(input); + if (time === null) { + return null; + } + return ((time.hour * 60 + time.minute) * 60 + time.second) * 1000 + time.millisecond; + }, + // https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local):concept-input-value-string-number + "datetime-local"(input) { + const dateAndTime = parseLocalDateAndTimeString(input); + if (dateAndTime === null) { + return null; + } + const { date: { year, month, day }, time: { hour, minute, second, millisecond } } = dateAndTime; + // Doesn't quite matter whether or not UTC is used, since the offset from 1970-01-01 local time is returned. + return getUTCMs(year, month, day, hour, minute, second, millisecond); + }, + // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-value-string-number + number: parseFloatingPointNumber, + // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-value-string-number + range: parseFloatingPointNumber +}; + +exports.convertStringToDateByType = { + date(input) { + const parsedInput = exports.convertStringToNumberByType.date(input); + return parsedInput === null ? null : new Date(parsedInput); + }, + // https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):concept-input-value-string-number + month(input) { + const parsedMonthString = parseMonthString(input); + if (parsedMonthString === null) { + return null; + } + + const date = new Date(0); + date.setUTCFullYear(parsedMonthString.year); + date.setUTCMonth(parsedMonthString.month - 1); + return date; + }, + week(input) { + const parsedInput = exports.convertStringToNumberByType.week(input); + return parsedInput === null ? null : new Date(parsedInput); + }, + time(input) { + const parsedInput = exports.convertStringToNumberByType.time(input); + return parsedInput === null ? null : new Date(parsedInput); + }, + "datetime-local"(input) { + const parsedInput = exports.convertStringToNumberByType["datetime-local"](input); + return parsedInput === null ? null : new Date(parsedInput); + } +}; + +exports.serializeDateByType = { + date(input) { + return serializeDate({ + year: input.getUTCFullYear(), + month: input.getUTCMonth() + 1, + day: input.getUTCDate() + }); + }, + month(input) { + return serializeMonth({ + year: input.getUTCFullYear(), + month: input.getUTCMonth() + 1 + }); + }, + week(input) { + return serializeWeek(parseDateAsWeek(input)); + }, + time(input) { + return serializeTime({ + hour: input.getUTCHours(), + minute: input.getUTCMinutes(), + second: input.getUTCSeconds(), + millisecond: input.getUTCMilliseconds() + }); + }, + "datetime-local"(input) { + return serializeNormalizedDateAndTime({ + date: { + year: input.getUTCFullYear(), + month: input.getUTCMonth() + 1, + day: input.getUTCDate() + }, + time: { + hour: input.getUTCHours(), + minute: input.getUTCMinutes(), + second: input.getUTCSeconds(), + millisecond: input.getUTCMilliseconds() + } + }); + } +}; + +exports.convertNumberToStringByType = { + // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):concept-input-value-string-number + date(input) { + return exports.serializeDateByType.date(new Date(input)); + }, + // https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):concept-input-value-string-date + month(input) { + const year = 1970 + Math.floor(input / 12); + const month = input % 12; + const date = new Date(0); + date.setUTCFullYear(year); + date.setUTCMonth(month); + + return exports.serializeDateByType.month(date); + }, + // https://html.spec.whatwg.org/multipage/input.html#week-state-(type=week):concept-input-value-string-date + week(input) { + return exports.serializeDateByType.week(new Date(input)); + }, + // https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):concept-input-value-string-date + time(input) { + return exports.serializeDateByType.time(new Date(input)); + }, + // https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local):concept-input-value-number-string + "datetime-local"(input) { + return exports.serializeDateByType["datetime-local"](new Date(input)); + }, + // https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):concept-input-value-number-string + number(input) { + return input.toString(); + }, + // https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):concept-input-value-number-string + range(input) { + return input.toString(); + } +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/ordered-set.js b/node_modules/jsdom/lib/jsdom/living/helpers/ordered-set.js new file mode 100644 index 0000000..d3e1932 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/ordered-set.js @@ -0,0 +1,104 @@ +"use strict"; + +// https://infra.spec.whatwg.org/#sets +// +// Only use this class if a Set cannot be used, e.g. when "replace" operation is needed, since there's no way to replace +// an element while keep the relative order using a Set, only remove and then add something at the end. + +module.exports = class OrderedSet { + constructor() { + this._items = []; + } + + append(item) { + if (!this.contains(item)) { + this._items.push(item); + } + } + + prepend(item) { + if (!this.contains(item)) { + this._items.unshift(item); + } + } + + replace(item, replacement) { + let seen = false; + for (let i = 0; i < this._items.length;) { + const isInstance = this._items[i] === item || this._items[i] === replacement; + if (seen && isInstance) { + this._items.splice(i, 1); + } else { + if (isInstance) { + this._items[i] = replacement; + seen = true; + } + i++; + } + } + } + + remove(...items) { + this.removePredicate(item => items.includes(item)); + } + + removePredicate(predicate) { + for (let i = 0; i < this._items.length;) { + if (predicate(this._items[i])) { + this._items.splice(i, 1); + } else { + i++; + } + } + } + + empty() { + this._items.length = 0; + } + + contains(item) { + return this._items.includes(item); + } + + get size() { + return this._items.length; + } + + isEmpty() { + return this._items.length === 0; + } + + // Useful for other parts of jsdom + + [Symbol.iterator]() { + return this._items[Symbol.iterator](); + } + + keys() { + return this._items.keys(); + } + + get(index) { + return this._items[index]; + } + + some(func) { + return this._items.some(func); + } + + // https://dom.spec.whatwg.org/#concept-ordered-set-parser + static parse(input) { + const tokens = new OrderedSet(); + for (const token of input.split(/[\t\n\f\r ]+/)) { + if (token) { + tokens.append(token); + } + } + return tokens; + } + + // https://dom.spec.whatwg.org/#concept-ordered-set-serializer + serialize() { + return this._items.join(" "); + } +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js b/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js new file mode 100644 index 0000000..41982b0 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js @@ -0,0 +1,76 @@ +"use strict"; +const util = require("util"); +const idlUtils = require("../generated/utils"); +const ErrorEvent = require("../generated/ErrorEvent"); +const { createAnEvent } = require("../helpers/events"); + +const errorReportingMode = Symbol("error reporting mode"); + +// https://html.spec.whatwg.org/multipage/webappapis.html#report-the-error +// Omits script parameter and any check for muted errors. +// Takes target as an EventTarget impl. +// Takes error object, message, and location as params, unlike the spec. +// Returns whether the event was handled or not. +function reportAnError(line, col, target, errorObject, message, location) { + if (target[errorReportingMode]) { + return false; + } + + target[errorReportingMode] = true; + + if (typeof message !== "string") { + message = "uncaught exception: " + util.inspect(errorObject); + } + + const event = createAnEvent("error", target._globalObject, ErrorEvent, { + cancelable: true, + message, + filename: location, + lineno: line, + colno: col, + error: errorObject + }); + + try { + target._dispatch(event); + } finally { + target[errorReportingMode] = false; + return event.defaultPrevented; + } +} + +module.exports = function reportException(window, error, filenameHint) { + // This function will give good results on real Error objects with stacks; poor ones otherwise + + const stack = error && error.stack; + const lines = stack && stack.split("\n"); + + // Find the first line that matches; important for multi-line messages + let pieces; + if (lines) { + for (let i = 1; i < lines.length && !pieces; ++i) { + pieces = lines[i].match(/at (?:(.+)\s+)?\(?(?:(.+?):(\d+):(\d+)|([^)]+))\)?/); + } + } + + const fileName = (pieces && pieces[2]) || filenameHint || window._document.URL; + const lineNumber = (pieces && parseInt(pieces[3])) || 0; + const columnNumber = (pieces && parseInt(pieces[4])) || 0; + + const windowImpl = idlUtils.implForWrapper(window); + + const handled = reportAnError(lineNumber, columnNumber, windowImpl, error, error && error.message, fileName); + + if (!handled) { + const errorString = shouldBeDisplayedAsError(error) ? `[${error.name}: ${error.message}]` : util.inspect(error); + const jsdomError = new Error(`Uncaught ${errorString}`); + jsdomError.detail = error; + jsdomError.type = "unhandled exception"; + + window._virtualConsole.emit("jsdomError", jsdomError); + } +}; + +function shouldBeDisplayedAsError(x) { + return x && x.name && x.message !== undefined && x.stack; +} diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/selectors.js b/node_modules/jsdom/lib/jsdom/living/helpers/selectors.js new file mode 100644 index 0000000..8f320ab --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/selectors.js @@ -0,0 +1,47 @@ +"use strict"; + +const nwsapi = require("nwsapi"); + +const idlUtils = require("../generated/utils"); + +function initNwsapi(node) { + const { _globalObject, _ownerDocument } = node; + + return nwsapi({ + document: _ownerDocument, + DOMException: _globalObject.DOMException + }); +} + +exports.matchesDontThrow = (elImpl, selector) => { + const document = elImpl._ownerDocument; + + if (!document._nwsapiDontThrow) { + document._nwsapiDontThrow = initNwsapi(elImpl); + document._nwsapiDontThrow.configure({ + LOGERRORS: false, + VERBOSITY: false, + IDS_DUPES: true, + MIXEDCASE: true + }); + } + + return document._nwsapiDontThrow.match(selector, idlUtils.wrapperForImpl(elImpl)); +}; + +// nwsapi gets `document.documentElement` at creation-time, so we have to initialize lazily, since in the initial +// stages of Document initialization, there is no documentElement present yet. +exports.addNwsapi = parentNode => { + const document = parentNode._ownerDocument; + + if (!document._nwsapi) { + document._nwsapi = initNwsapi(parentNode); + document._nwsapi.configure({ + LOGERRORS: false, + IDS_DUPES: true, + MIXEDCASE: true + }); + } + + return document._nwsapi; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/shadow-dom.js b/node_modules/jsdom/lib/jsdom/living/helpers/shadow-dom.js new file mode 100644 index 0000000..a88a33e --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/shadow-dom.js @@ -0,0 +1,285 @@ +"use strict"; + +const NODE_TYPE = require("../node-type"); + +const { nodeRoot } = require("./node"); +const { HTML_NS } = require("./namespaces"); +const { domSymbolTree } = require("./internal-constants"); +const { signalSlotList, queueMutationObserverMicrotask } = require("./mutation-observers"); + +// Valid host element for ShadowRoot. +// Defined in: https://dom.spec.whatwg.org/#dom-element-attachshadow +const VALID_HOST_ELEMENT_NAME = new Set([ + "article", + "aside", + "blockquote", + "body", + "div", + "footer", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "header", + "main", + "nav", + "p", + "section", + "span" +]); + +function isValidHostElementName(name) { + return VALID_HOST_ELEMENT_NAME.has(name); +} + +// Use an approximation by checking the presence of nodeType instead of instead of using the isImpl from +// "../generated/Node" to avoid introduction of circular dependencies. +function isNode(nodeImpl) { + return Boolean(nodeImpl && "nodeType" in nodeImpl); +} + +// Use an approximation by checking the value of nodeType and presence of nodeType host instead of instead +// of using the isImpl from "../generated/ShadowRoot" to avoid introduction of circular dependencies. +function isShadowRoot(nodeImpl) { + return Boolean(nodeImpl && nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE && "host" in nodeImpl); +} + +// https://dom.spec.whatwg.org/#concept-slotable +function isSlotable(nodeImpl) { + return nodeImpl && (nodeImpl.nodeType === NODE_TYPE.ELEMENT_NODE || nodeImpl.nodeType === NODE_TYPE.TEXT_NODE); +} + +function isSlot(nodeImpl) { + return nodeImpl && nodeImpl.localName === "slot" && nodeImpl._namespaceURI === HTML_NS; +} + +// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor +function isShadowInclusiveAncestor(ancestor, node) { + while (isNode(node)) { + if (node === ancestor) { + return true; + } + + if (isShadowRoot(node)) { + node = node.host; + } else { + node = domSymbolTree.parent(node); + } + } + + return false; +} + +// https://dom.spec.whatwg.org/#retarget +function retarget(a, b) { + while (true) { + if (!isNode(a)) { + return a; + } + + const aRoot = nodeRoot(a); + if ( + !isShadowRoot(aRoot) || + (isNode(b) && isShadowInclusiveAncestor(aRoot, b)) + ) { + return a; + } + + a = nodeRoot(a).host; + } +} + +// https://dom.spec.whatwg.org/#get-the-parent +function getEventTargetParent(eventTarget, event) { + // _getTheParent will be missing for Window, since it doesn't have an impl class and we don't want to pollute the + // user-visible global scope with a _getTheParent value. TODO: remove this entire function and use _getTheParent + // directly, once Window gets split into impl/wrapper. + return eventTarget._getTheParent ? eventTarget._getTheParent(event) : null; +} + +// https://dom.spec.whatwg.org/#concept-shadow-including-root +function shadowIncludingRoot(node) { + const root = nodeRoot(node); + return isShadowRoot(root) ? shadowIncludingRoot(root.host) : root; +} + +// https://dom.spec.whatwg.org/#assign-a-slot +function assignSlot(slotable) { + const slot = findSlot(slotable); + + if (slot) { + assignSlotable(slot); + } +} + +// https://dom.spec.whatwg.org/#assign-slotables +function assignSlotable(slot) { + const slotables = findSlotable(slot); + + let shouldFireSlotChange = false; + + if (slotables.length !== slot._assignedNodes.length) { + shouldFireSlotChange = true; + } else { + for (let i = 0; i < slotables.length; i++) { + if (slotables[i] !== slot._assignedNodes[i]) { + shouldFireSlotChange = true; + break; + } + } + } + + if (shouldFireSlotChange) { + signalSlotChange(slot); + } + + slot._assignedNodes = slotables; + + for (const slotable of slotables) { + slotable._assignedSlot = slot; + } +} + +// https://dom.spec.whatwg.org/#assign-slotables-for-a-tree +function assignSlotableForTree(root) { + for (const slot of domSymbolTree.treeIterator(root)) { + if (isSlot(slot)) { + assignSlotable(slot); + } + } +} + +// https://dom.spec.whatwg.org/#find-slotables +function findSlotable(slot) { + const result = []; + + const root = nodeRoot(slot); + if (!isShadowRoot(root)) { + return result; + } + + for (const slotable of domSymbolTree.treeIterator(root.host)) { + const foundSlot = findSlot(slotable); + + if (foundSlot === slot) { + result.push(slotable); + } + } + + return result; +} + +// https://dom.spec.whatwg.org/#find-flattened-slotables +function findFlattenedSlotables(slot) { + const result = []; + + const root = nodeRoot(slot); + if (!isShadowRoot(root)) { + return result; + } + + const slotables = findSlotable(slot); + + if (slotables.length === 0) { + for (const child of domSymbolTree.childrenIterator(slot)) { + if (isSlotable(child)) { + slotables.push(child); + } + } + } + + for (const node of slotables) { + if (isSlot(node) && isShadowRoot(nodeRoot(node))) { + const temporaryResult = findFlattenedSlotables(node); + result.push(...temporaryResult); + } else { + result.push(node); + } + } + + return result; +} + +// https://dom.spec.whatwg.org/#find-a-slot +function findSlot(slotable, openFlag) { + const { parentNode: parent } = slotable; + + if (!parent) { + return null; + } + + const shadow = parent._shadowRoot; + + if (!shadow || (openFlag && shadow.mode !== "open")) { + return null; + } + + for (const child of domSymbolTree.treeIterator(shadow)) { + if (isSlot(child) && child.name === slotable._slotableName) { + return child; + } + } + + return null; +} + +// https://dom.spec.whatwg.org/#signal-a-slot-change +function signalSlotChange(slot) { + if (!signalSlotList.some(entry => entry === slot)) { + signalSlotList.push(slot); + } + + queueMutationObserverMicrotask(); +} + +// https://dom.spec.whatwg.org/#concept-shadow-including-descendant +function* shadowIncludingInclusiveDescendantsIterator(node) { + yield node; + + if (node._shadowRoot) { + yield* shadowIncludingInclusiveDescendantsIterator(node._shadowRoot); + } + + for (const child of domSymbolTree.childrenIterator(node)) { + yield* shadowIncludingInclusiveDescendantsIterator(child); + } +} + +// https://dom.spec.whatwg.org/#concept-shadow-including-descendant +function* shadowIncludingDescendantsIterator(node) { + if (node._shadowRoot) { + yield* shadowIncludingInclusiveDescendantsIterator(node._shadowRoot); + } + + for (const child of domSymbolTree.childrenIterator(node)) { + yield* shadowIncludingInclusiveDescendantsIterator(child); + } +} + +module.exports = { + isValidHostElementName, + + isNode, + isSlotable, + isSlot, + isShadowRoot, + + isShadowInclusiveAncestor, + retarget, + getEventTargetParent, + shadowIncludingRoot, + + assignSlot, + assignSlotable, + assignSlotableForTree, + + findSlot, + findFlattenedSlotables, + + signalSlotChange, + + shadowIncludingInclusiveDescendantsIterator, + shadowIncludingDescendantsIterator +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/strings.js b/node_modules/jsdom/lib/jsdom/living/helpers/strings.js new file mode 100644 index 0000000..1579251 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/strings.js @@ -0,0 +1,148 @@ +"use strict"; + +// https://infra.spec.whatwg.org/#ascii-whitespace +const asciiWhitespaceRe = /^[\t\n\f\r ]$/; +exports.asciiWhitespaceRe = asciiWhitespaceRe; + +// https://infra.spec.whatwg.org/#ascii-lowercase +exports.asciiLowercase = s => { + return s.replace(/[A-Z]/g, l => l.toLowerCase()); +}; + +// https://infra.spec.whatwg.org/#ascii-uppercase +exports.asciiUppercase = s => { + return s.replace(/[a-z]/g, l => l.toUpperCase()); +}; + +// https://infra.spec.whatwg.org/#strip-newlines +exports.stripNewlines = s => { + return s.replace(/[\n\r]+/g, ""); +}; + +// https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace +exports.stripLeadingAndTrailingASCIIWhitespace = s => { + return s.replace(/^[ \t\n\f\r]+/, "").replace(/[ \t\n\f\r]+$/, ""); +}; + +// https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace +exports.stripAndCollapseASCIIWhitespace = s => { + return s.replace(/[ \t\n\f\r]+/g, " ").replace(/^[ \t\n\f\r]+/, "").replace(/[ \t\n\f\r]+$/, ""); +}; + +// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-simple-colour +exports.isValidSimpleColor = s => { + return /^#[a-fA-F\d]{6}$/.test(s); +}; + +// https://infra.spec.whatwg.org/#ascii-case-insensitive +exports.asciiCaseInsensitiveMatch = (a, b) => { + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; ++i) { + if ((a.charCodeAt(i) | 32) !== (b.charCodeAt(i) | 32)) { + return false; + } + } + + return true; +}; + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers +// Error is represented as null. +const parseInteger = exports.parseInteger = input => { + // The implementation here is slightly different from the spec's. We want to use parseInt(), but parseInt() trims + // Unicode whitespace in addition to just ASCII ones, so we make sure that the trimmed prefix contains only ASCII + // whitespace ourselves. + const numWhitespace = input.length - input.trimStart().length; + if (/[^\t\n\f\r ]/.test(input.slice(0, numWhitespace))) { + return null; + } + // We don't allow hexadecimal numbers here. + // eslint-disable-next-line radix + const value = parseInt(input, 10); + if (Number.isNaN(value)) { + return null; + } + // parseInt() returns -0 for "-0". Normalize that here. + return value === 0 ? 0 : value; +}; + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-non-negative-integers +// Error is represented as null. +exports.parseNonNegativeInteger = input => { + const value = parseInteger(input); + if (value === null) { + return null; + } + if (value < 0) { + return null; + } + return value; +}; + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-floating-point-number +const floatingPointNumRe = /^-?(?:\d+|\d*\.\d+)(?:[eE][-+]?\d+)?$/; +exports.isValidFloatingPointNumber = str => floatingPointNumRe.test(str); + +// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-floating-point-number-values +// Error is represented as null. +exports.parseFloatingPointNumber = str => { + // The implementation here is slightly different from the spec's. We need to use parseFloat() in order to retain + // accuracy, but parseFloat() trims Unicode whitespace in addition to just ASCII ones, so we make sure that the + // trimmed prefix contains only ASCII whitespace ourselves. + const numWhitespace = str.length - str.trimStart().length; + if (/[^\t\n\f\r ]/.test(str.slice(0, numWhitespace))) { + return null; + } + const parsed = parseFloat(str); + return isFinite(parsed) ? parsed : null; +}; + +// https://infra.spec.whatwg.org/#split-on-ascii-whitespace +exports.splitOnASCIIWhitespace = str => { + let position = 0; + const tokens = []; + while (position < str.length && asciiWhitespaceRe.test(str[position])) { + position++; + } + if (position === str.length) { + return tokens; + } + while (position < str.length) { + const start = position; + while (position < str.length && !asciiWhitespaceRe.test(str[position])) { + position++; + } + tokens.push(str.slice(start, position)); + while (position < str.length && asciiWhitespaceRe.test(str[position])) { + position++; + } + } + return tokens; +}; + +// https://infra.spec.whatwg.org/#split-on-commas +exports.splitOnCommas = str => { + let position = 0; + const tokens = []; + while (position < str.length) { + let start = position; + while (position < str.length && str[position] !== ",") { + position++; + } + let end = position; + while (start < str.length && asciiWhitespaceRe.test(str[start])) { + start++; + } + while (end > start && asciiWhitespaceRe.test(str[end - 1])) { + end--; + } + tokens.push(str.slice(start, end)); + if (position < str.length) { + position++; + } + } + return tokens; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js b/node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js new file mode 100644 index 0000000..63e749d --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/style-rules.js @@ -0,0 +1,114 @@ +"use strict"; +const cssom = require("cssom"); +const defaultStyleSheet = require("../../browser/default-stylesheet"); +const { matchesDontThrow } = require("./selectors"); + +const { forEach, indexOf } = Array.prototype; + +let parsedDefaultStyleSheet; + +// Properties for which getResolvedValue is implemented. This is less than +// every supported property. +// https://drafts.csswg.org/indexes/#properties +exports.propertiesWithResolvedValueImplemented = { + __proto__: null, + + // https://drafts.csswg.org/css2/visufx.html#visibility + visibility: { + inherited: true, + initial: "visible", + computedValue: "as-specified" + } +}; + +exports.forEachMatchingSheetRuleOfElement = (elementImpl, handleRule) => { + function handleSheet(sheet) { + forEach.call(sheet.cssRules, rule => { + if (rule.media) { + if (indexOf.call(rule.media, "screen") !== -1) { + forEach.call(rule.cssRules, innerRule => { + if (matches(innerRule, elementImpl)) { + handleRule(innerRule); + } + }); + } + } else if (matches(rule, elementImpl)) { + handleRule(rule); + } + }); + } + + if (!parsedDefaultStyleSheet) { + parsedDefaultStyleSheet = cssom.parse(defaultStyleSheet); + } + + handleSheet(parsedDefaultStyleSheet); + forEach.call(elementImpl._ownerDocument.styleSheets._list, handleSheet); +}; + +function matches(rule, element) { + return matchesDontThrow(element, rule.selectorText); +} + +// Naive implementation of https://drafts.csswg.org/css-cascade-4/#cascading +// based on the previous jsdom implementation of getComputedStyle. +// Does not implement https://drafts.csswg.org/css-cascade-4/#cascade-specificity, +// or rather specificity is only implemented by the order in which the matching +// rules appear. The last rule is the most specific while the first rule is +// the least specific. +function getCascadedPropertyValue(element, property) { + let value = ""; + + exports.forEachMatchingSheetRuleOfElement(element, rule => { + const propertyValue = rule.style.getPropertyValue(property); + // getPropertyValue returns "" if the property is not found + if (propertyValue !== "") { + value = propertyValue; + } + }); + + const inlineValue = element.style.getPropertyValue(property); + if (inlineValue !== "" && inlineValue !== null) { + value = inlineValue; + } + + return value; +} + +// https://drafts.csswg.org/css-cascade-4/#specified-value +function getSpecifiedValue(element, property) { + const cascade = getCascadedPropertyValue(element, property); + + if (cascade !== "") { + return cascade; + } + + // Defaulting + const { initial, inherited } = exports.propertiesWithResolvedValueImplemented[property]; + if (inherited && element.parentElement !== null) { + return getComputedValue(element.parentElement, property); + } + + // root element without parent element or inherited property + return initial; +} + +// https://drafts.csswg.org/css-cascade-4/#computed-value +function getComputedValue(element, property) { + const { computedValue } = exports.propertiesWithResolvedValueImplemented[property]; + if (computedValue === "as-specified") { + return getSpecifiedValue(element, property); + } + + throw new TypeError(`Internal error: unrecognized computed value instruction '${computedValue}'`); +} + +// https://drafts.csswg.org/cssom/#resolved-value +// Only implements `visibility` +exports.getResolvedValue = (element, property) => { + // Determined for special case properties, none of which are implemented here. + // So we skip to "any other property: The resolved value is the computed value." + return getComputedValue(element, property); +}; + +exports.SHADOW_DOM_PSEUDO_REGEXP = /^::(?:part|slotted)\(/i; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/stylesheets.js b/node_modules/jsdom/lib/jsdom/living/helpers/stylesheets.js new file mode 100644 index 0000000..7138599 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/stylesheets.js @@ -0,0 +1,113 @@ +"use strict"; +const cssom = require("cssom"); +const whatwgEncoding = require("whatwg-encoding"); +const whatwgURL = require("whatwg-url"); + +// TODO: this should really implement https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet +// It (and the things it calls) is nowhere close right now. +exports.fetchStylesheet = (elementImpl, urlString) => { + const parsedURL = whatwgURL.parseURL(urlString); + return fetchStylesheetInternal(elementImpl, urlString, parsedURL); +}; + +// https://drafts.csswg.org/cssom/#remove-a-css-style-sheet +exports.removeStylesheet = (sheet, elementImpl) => { + const { styleSheets } = elementImpl._ownerDocument; + styleSheets._remove(sheet); + + // Remove the association explicitly; in the spec it's implicit so this step doesn't exist. + elementImpl.sheet = null; + + // TODO: "Set the CSS style sheet’s parent CSS style sheet, owner node and owner CSS rule to null." + // Probably when we have a real CSSOM implementation. +}; + +// https://drafts.csswg.org/cssom/#create-a-css-style-sheet kinda: +// - Parsing failures are not handled gracefully like they should be +// - The import rules stuff seems out of place, and probably should affect the load event... +exports.createStylesheet = (sheetText, elementImpl, baseURL) => { + let sheet; + try { + sheet = cssom.parse(sheetText); + } catch (e) { + if (elementImpl._ownerDocument._defaultView) { + const error = new Error("Could not parse CSS stylesheet"); + error.detail = sheetText; + error.type = "css parsing"; + + elementImpl._ownerDocument._defaultView._virtualConsole.emit("jsdomError", error); + } + return; + } + + scanForImportRules(elementImpl, sheet.cssRules, baseURL); + + addStylesheet(sheet, elementImpl); +}; + +// https://drafts.csswg.org/cssom/#add-a-css-style-sheet +function addStylesheet(sheet, elementImpl) { + elementImpl._ownerDocument.styleSheets._add(sheet); + + // Set the association explicitly; in the spec it's implicit. + elementImpl.sheet = sheet; + + // TODO: title and disabled stuff +} + +function fetchStylesheetInternal(elementImpl, urlString, parsedURL) { + const document = elementImpl._ownerDocument; + let defaultEncoding = document._encoding; + const resourceLoader = document._resourceLoader; + + if (elementImpl.localName === "link" && elementImpl.hasAttributeNS(null, "charset")) { + defaultEncoding = whatwgEncoding.labelToName(elementImpl.getAttributeNS(null, "charset")); + } + + function onStylesheetLoad(data) { + const css = whatwgEncoding.decode(data, defaultEncoding); + + // TODO: MIME type checking? + if (elementImpl.sheet) { + exports.removeStylesheet(elementImpl.sheet, elementImpl); + } + exports.createStylesheet(css, elementImpl, parsedURL); + } + + resourceLoader.fetch(urlString, { + element: elementImpl, + onLoad: onStylesheetLoad + }); +} + +// TODO this is actually really messed up and overwrites the sheet on elementImpl +// Tracking in https://github.com/jsdom/jsdom/issues/2124 +function scanForImportRules(elementImpl, cssRules, baseURL) { + if (!cssRules) { + return; + } + + for (let i = 0; i < cssRules.length; ++i) { + if (cssRules[i].cssRules) { + // @media rule: keep searching inside it. + scanForImportRules(elementImpl, cssRules[i].cssRules, baseURL); + } else if (cssRules[i].href) { + // @import rule: fetch the resource and evaluate it. + // See http://dev.w3.org/csswg/cssom/#css-import-rule + // If loading of the style sheet fails its cssRules list is simply + // empty. I.e. an @import rule always has an associated style sheet. + const parsed = whatwgURL.parseURL(cssRules[i].href, { baseURL }); + if (parsed === null) { + const window = elementImpl._ownerDocument._defaultView; + if (window) { + const error = new Error(`Could not parse CSS @import URL ${cssRules[i].href} relative to base URL ` + + `"${whatwgURL.serializeURL(baseURL)}"`); + error.type = "css @import URL parsing"; + window._virtualConsole.emit("jsdomError", error); + } + } else { + fetchStylesheetInternal(elementImpl, whatwgURL.serializeURL(parsed), parsed); + } + } + } +} diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/svg/basic-types.js b/node_modules/jsdom/lib/jsdom/living/helpers/svg/basic-types.js new file mode 100644 index 0000000..16d0dc1 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/svg/basic-types.js @@ -0,0 +1,41 @@ +"use strict"; + +// https://svgwg.org/svg2-draft/types.html#TermDetach +function detach(value) { + if (typeof value === "string") { + return; + } + + throw new TypeError(`jsdom internal error: detaching object of wrong type ${value}`); +} +exports.detach = detach; + +// https://svgwg.org/svg2-draft/types.html#TermAttach +// listObject corresponds to the parameter taken by the algorithm in the spec, but is currently unused because only +// DOMString type is supported by jsdom (and this function) right now. +// eslint-disable-next-line no-unused-vars +function attach(value, listObject) { + if (typeof value === "string") { + return; + } + + throw new TypeError(`jsdom internal error: attaching object of wrong type ${value}`); +} +exports.attach = attach; + +// https://svgwg.org/svg2-draft/types.html#TermReserialize for DOMString. +function reserializeSpaceSeparatedTokens(elements) { + return elements.join(" "); +} +exports.reserializeSpaceSeparatedTokens = reserializeSpaceSeparatedTokens; + +// Used for systemLanguage attribute, whose value is a set of comma-separated tokens: +// https://svgwg.org/svg2-draft/struct.html#SystemLanguageAttribute +// SVG 2 spec (https://svgwg.org/svg2-draft/types.html#TermReserialize) says any SVGStringList should reserialize the +// same way, as space-separated tokens, but doing so for systemLanguage is illogical and contradicts the Firefox +// behavior. +// I cannot find a description of reserialization of SVGStringList in the SVG 1.1 spec. +function reserializeCommaSeparatedTokens(elements) { + return elements.join(", "); +} +exports.reserializeCommaSeparatedTokens = reserializeCommaSeparatedTokens; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/svg/render.js b/node_modules/jsdom/lib/jsdom/living/helpers/svg/render.js new file mode 100644 index 0000000..651568d --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/svg/render.js @@ -0,0 +1,46 @@ +"use strict"; +const { SVG_NS } = require("../namespaces"); + +// https://svgwg.org/svg2-draft/render.html#TermNeverRenderedElement +const neverRenderedElements = new Set([ + "clipPath", + "defs", + "desc", + "linearGradient", + "marker", + "mask", + "metadata", + "pattern", + "radialGradient", + "script", + "style", + "title", + "symbol" +]); + +// https://svgwg.org/svg2-draft/render.html#Rendered-vs-NonRendered +exports.isRenderedElement = elImpl => { + if (neverRenderedElements.has(elImpl._localName)) { + return false; + } + + // This does not check for elements excluded because of conditional processing attributes or ‘switch’ structures, + // because conditional processing is not implemented. + // https://svgwg.org/svg2-draft/struct.html#ConditionalProcessing + + // This does not check for computed style of display being none, since that is not yet implemented for HTML + // focusability either (and there are no tests yet). + + if (!elImpl.isConnected) { + return false; + } + + // The spec is unclear about how to deal with non-SVG parents, so we only perform this check for SVG-namespace + // parents. + if (elImpl.parentElement && elImpl.parentElement._namespaceURI === SVG_NS && + !exports.isRenderedElement(elImpl.parentNode)) { + return false; + } + + return true; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/text.js b/node_modules/jsdom/lib/jsdom/living/helpers/text.js new file mode 100644 index 0000000..632c0e5 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/text.js @@ -0,0 +1,19 @@ +"use strict"; +const { domSymbolTree } = require("./internal-constants"); +const { CDATA_SECTION_NODE, TEXT_NODE } = require("../node-type"); + +// +// https://dom.spec.whatwg.org/#concept-child-text-content +// +exports.childTextContent = node => { + let result = ""; + const iterator = domSymbolTree.childrenIterator(node); + for (const child of iterator) { + if (child.nodeType === TEXT_NODE || + // The CDataSection extends Text. + child.nodeType === CDATA_SECTION_NODE) { + result += child.data; + } + } + return result; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/traversal.js b/node_modules/jsdom/lib/jsdom/living/helpers/traversal.js new file mode 100644 index 0000000..91f7148 --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/traversal.js @@ -0,0 +1,72 @@ +"use strict"; +const { domSymbolTree } = require("./internal-constants"); +const { HTML_NS } = require("./namespaces"); + +// All these operate on and return impls, not wrappers! + +exports.closest = (e, localName, namespace = HTML_NS) => { + while (e) { + if (e.localName === localName && e.namespaceURI === namespace) { + return e; + } + e = domSymbolTree.parent(e); + } + + return null; +}; + +exports.childrenByLocalName = (parent, localName, namespace = HTML_NS) => { + return domSymbolTree.childrenToArray(parent, { filter(node) { + return node._localName === localName && node._namespaceURI === namespace; + } }); +}; + +exports.descendantsByLocalName = (parent, localName, namespace = HTML_NS) => { + return domSymbolTree.treeToArray(parent, { filter(node) { + return node._localName === localName && node._namespaceURI === namespace && node !== parent; + } }); +}; + +exports.childrenByLocalNames = (parent, localNamesSet, namespace = HTML_NS) => { + return domSymbolTree.childrenToArray(parent, { filter(node) { + return localNamesSet.has(node._localName) && node._namespaceURI === namespace; + } }); +}; + +exports.descendantsByLocalNames = (parent, localNamesSet, namespace = HTML_NS) => { + return domSymbolTree.treeToArray(parent, { filter(node) { + return localNamesSet.has(node._localName) && + node._namespaceURI === namespace && + node !== parent; + } }); +}; + +exports.firstChildWithLocalName = (parent, localName, namespace = HTML_NS) => { + const iterator = domSymbolTree.childrenIterator(parent); + for (const child of iterator) { + if (child._localName === localName && child._namespaceURI === namespace) { + return child; + } + } + return null; +}; + +exports.firstChildWithLocalNames = (parent, localNamesSet, namespace = HTML_NS) => { + const iterator = domSymbolTree.childrenIterator(parent); + for (const child of iterator) { + if (localNamesSet.has(child._localName) && child._namespaceURI === namespace) { + return child; + } + } + return null; +}; + +exports.firstDescendantWithLocalName = (parent, localName, namespace = HTML_NS) => { + const iterator = domSymbolTree.treeIterator(parent); + for (const descendant of iterator) { + if (descendant._localName === localName && descendant._namespaceURI === namespace) { + return descendant; + } + } + return null; +}; diff --git a/node_modules/jsdom/lib/jsdom/living/helpers/validate-names.js b/node_modules/jsdom/lib/jsdom/living/helpers/validate-names.js new file mode 100644 index 0000000..d341dbd --- /dev/null +++ b/node_modules/jsdom/lib/jsdom/living/helpers/validate-names.js @@ -0,0 +1,75 @@ +"use strict"; +const xnv = require("xml-name-validator"); +const DOMException = require("domexception/webidl2js-wrapper"); +const { XML_NS, XMLNS_NS } = require("../helpers/namespaces"); + +// https://dom.spec.whatwg.org/#validate + +exports.name = function (globalObject, name) { + const result = xnv.name(name); + if (!result.success) { + throw DOMException.create(globalObject, [ + `"${name}" did not match the Name production: ${result.error}`, + "InvalidCharacterError" + ]); + } +}; + +exports.qname = function (globalObject, qname) { + exports.name(globalObject, qname); + + const result = xnv.qname(qname); + if (!result.success) { + throw DOMException.create(globalObject, [ + `"${qname}" did not match the QName production: ${result.error}`, + "InvalidCharacterError" + ]); + } +}; + +exports.validateAndExtract = function (globalObject, namespace, qualifiedName) { + if (namespace === "") { + namespace = null; + } + + exports.qname(globalObject, qualifiedName); + + let prefix = null; + let localName = qualifiedName; + + const colonIndex = qualifiedName.indexOf(":"); + if (colonIndex !== -1) { + prefix = qualifiedName.substring(0, colonIndex); + localName = qualifiedName.substring(colonIndex + 1); + } + + if (prefix !== null && namespace === null) { + throw DOMException.create(globalObject, [ + "A namespace was given but a prefix was also extracted from the qualifiedName", + "NamespaceError" + ]); + } + + if (prefix === "xml" && namespace !== XML_NS) { + throw DOMException.create(globalObject, [ + "A prefix of \"xml\" was given but the namespace was not the XML namespace", + "NamespaceError" + ]); + } + + if ((qualifiedName === "xmlns" || prefix === "xmlns") && namespace !== XMLNS_NS) { + throw DOMException.create(globalObject, [ + "A prefix or qualifiedName of \"xmlns\" was given but the namespace was not the XMLNS namespace", + "NamespaceError" + ]); + } + + if (namespace === XMLNS_NS && qualifiedName !== "xmlns" && prefix !== "xmlns") { + throw DOMException.create(globalObject, [ + "The XMLNS namespace was given but neither the prefix nor qualifiedName was \"xmlns\"", + "NamespaceError" + ]); + } + + return { namespace, prefix, localName }; +}; |