This commit is contained in:
eric sciple 2020-01-24 12:21:24 -05:00
parent fc725ba36b
commit 422b9fdb15
7395 changed files with 1786235 additions and 3476 deletions

704
node_modules/jsdom/lib/jsdom/browser/Window.js generated vendored Normal file
View file

@ -0,0 +1,704 @@
"use strict";
const webIDLConversions = require("webidl-conversions");
const { CSSStyleDeclaration } = require("cssstyle");
const { Performance: RawPerformance } = require("w3c-hr-time");
const notImplemented = require("./not-implemented");
const VirtualConsole = require("../virtual-console");
const { define, mixin } = require("../utils");
const EventTarget = require("../living/generated/EventTarget");
const namedPropertiesWindow = require("../living/named-properties-window");
const cssom = require("cssom");
const postMessage = require("../living/post-message");
const DOMException = require("domexception");
const { btoa, atob } = require("abab");
const idlUtils = require("../living/generated/utils");
const createXMLHttpRequest = require("../living/xmlhttprequest");
const createFileReader = require("../living/generated/FileReader").createInterface;
const createWebSocket = require("../living/generated/WebSocket").createInterface;
const WebSocketImpl = require("../living/websockets/WebSocket-impl").implementation;
const BarProp = require("../living/generated/BarProp");
const Document = require("../living/generated/Document");
const External = require("../living/generated/External");
const Navigator = require("../living/generated/Navigator");
const Performance = require("../living/generated/Performance");
const Screen = require("../living/generated/Screen");
const Storage = require("../living/generated/Storage");
const createAbortController = require("../living/generated/AbortController").createInterface;
const createAbortSignal = require("../living/generated/AbortSignal").createInterface;
const reportException = require("../living/helpers/runtime-script-errors");
const { matchesDontThrow } = require("../living/helpers/selectors");
const SessionHistory = require("../living/window/SessionHistory");
const { contextifyWindow } = require("./documentfeatures.js");
const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation;
const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation;
// NB: the require() must be after assigning `module.exports` because this require() is circular
// TODO: this above note might not even be true anymore... figure out the cycle and document it, or clean up.
module.exports = Window;
const dom = require("../living");
const cssSelectorSplitRE = /((?:[^,"']|"[^"]*"|'[^']*')+)/;
const defaultStyleSheet = cssom.parse(require("./default-stylesheet"));
dom.Window = Window;
// NOTE: per https://heycam.github.io/webidl/#Global, all properties on the Window object must be own-properties.
// That is why we assign everything inside of the constructor, instead of using a shared prototype.
// You can verify this in e.g. Firefox or Internet Explorer, which do a good job with Web IDL compliance.
function Window(options) {
EventTarget.setup(this);
const rawPerformance = new RawPerformance();
const windowInitialized = rawPerformance.now();
const window = this;
mixin(window, WindowEventHandlersImpl.prototype);
mixin(window, GlobalEventHandlersImpl.prototype);
this._initGlobalEvents();
///// INTERFACES FROM THE DOM
// TODO: consider a mode of some sort where these are not shared between all DOM instances
// It'd be very memory-expensive in most cases, though.
for (const name in dom) {
Object.defineProperty(window, name, {
enumerable: false,
configurable: true,
writable: true,
value: dom[name]
});
}
///// PRIVATE DATA PROPERTIES
// vm initialization is deferred until script processing is activated
this._globalProxy = this;
Object.defineProperty(idlUtils.implForWrapper(this), idlUtils.wrapperSymbol, { get: () => this._globalProxy });
let timers = Object.create(null);
let animationFrameCallbacks = Object.create(null);
// List options explicitly to be clear which are passed through
this._document = Document.create([], {
options: {
parsingMode: options.parsingMode,
contentType: options.contentType,
encoding: options.encoding,
cookieJar: options.cookieJar,
url: options.url,
lastModified: options.lastModified,
referrer: options.referrer,
cookie: options.cookie,
deferClose: options.deferClose,
resourceLoader: options.resourceLoader,
concurrentNodeIterators: options.concurrentNodeIterators,
pool: options.pool,
agent: options.agent,
agentClass: options.agentClass,
agentOptions: options.agentOptions,
strictSSL: options.strictSSL,
proxy: options.proxy,
parseOptions: options.parseOptions,
defaultView: this._globalProxy,
global: this
}
});
// https://html.spec.whatwg.org/#session-history
this._sessionHistory = new SessionHistory({
document: idlUtils.implForWrapper(this._document),
url: idlUtils.implForWrapper(this._document)._URL,
stateObject: null
}, this);
// TODO NEWAPI can remove this
if (options.virtualConsole) {
if (options.virtualConsole instanceof VirtualConsole) {
this._virtualConsole = options.virtualConsole;
} else {
throw new TypeError("options.virtualConsole must be a VirtualConsole (from createVirtualConsole)");
}
} else {
this._virtualConsole = new VirtualConsole();
}
this._runScripts = options.runScripts;
if (this._runScripts === "outside-only" || this._runScripts === "dangerously") {
contextifyWindow(this);
}
// Set up the window as if it's a top level window.
// If it's not, then references will be corrected by frame/iframe code.
this._parent = this._top = this._globalProxy;
this._frameElement = null;
// This implements window.frames.length, since window.frames returns a
// self reference to the window object. This value is incremented in the
// HTMLFrameElement implementation.
this._length = 0;
this._pretendToBeVisual = options.pretendToBeVisual;
this._storageQuota = options.storageQuota;
// Some properties (such as localStorage and sessionStorage) share data
// between windows in the same origin. This object is intended
// to contain such data.
if (options.commonForOrigin && options.commonForOrigin[this._document.origin]) {
this._commonForOrigin = options.commonForOrigin;
} else {
this._commonForOrigin = {
[this._document.origin]: {
localStorageArea: new Map(),
sessionStorageArea: new Map(),
windowsInSameOrigin: [this]
}
};
}
this._currentOriginData = this._commonForOrigin[this._document.origin];
///// WEB STORAGE
this._localStorage = Storage.create([], {
associatedWindow: this,
storageArea: this._currentOriginData.localStorageArea,
type: "localStorage",
url: this._document.documentURI,
storageQuota: this._storageQuota
});
this._sessionStorage = Storage.create([], {
associatedWindow: this,
storageArea: this._currentOriginData.sessionStorageArea,
type: "sessionStorage",
url: this._document.documentURI,
storageQuota: this._storageQuota
});
///// GETTERS
const locationbar = BarProp.create();
const menubar = BarProp.create();
const personalbar = BarProp.create();
const scrollbars = BarProp.create();
const statusbar = BarProp.create();
const toolbar = BarProp.create();
const external = External.create();
const navigator = Navigator.create([], { userAgent: options.userAgent });
const performance = Performance.create([], { rawPerformance });
const screen = Screen.create();
define(this, {
get length() {
return window._length;
},
get window() {
return window._globalProxy;
},
get frameElement() {
return window._frameElement;
},
get frames() {
return window._globalProxy;
},
get self() {
return window._globalProxy;
},
get parent() {
return window._parent;
},
get top() {
return window._top;
},
get document() {
return window._document;
},
get external() {
return external;
},
get location() {
return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._location);
},
get history() {
return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._history);
},
get navigator() {
return navigator;
},
get locationbar() {
return locationbar;
},
get menubar() {
return menubar;
},
get personalbar() {
return personalbar;
},
get scrollbars() {
return scrollbars;
},
get statusbar() {
return statusbar;
},
get toolbar() {
return toolbar;
},
get performance() {
return performance;
},
get screen() {
return screen;
},
get localStorage() {
if (this._document.origin === "null") {
throw new DOMException("localStorage is not available for opaque origins", "SecurityError");
}
return this._localStorage;
},
get sessionStorage() {
if (this._document.origin === "null") {
throw new DOMException("sessionStorage is not available for opaque origins", "SecurityError");
}
return this._sessionStorage;
}
});
namedPropertiesWindow.initializeWindow(this, this._globalProxy);
///// METHODS for [ImplicitThis] hack
// See https://lists.w3.org/Archives/Public/public-script-coord/2015JanMar/0109.html
this.addEventListener = this.addEventListener.bind(this);
this.removeEventListener = this.removeEventListener.bind(this);
this.dispatchEvent = this.dispatchEvent.bind(this);
///// METHODS
let latestTimerId = 0;
let latestAnimationFrameCallbackId = 0;
this.setTimeout = function (fn, ms) {
const args = [];
for (let i = 2; i < arguments.length; ++i) {
args[i - 2] = arguments[i];
}
return startTimer(window, setTimeout, clearTimeout, ++latestTimerId, fn, ms, timers, args);
};
this.setInterval = function (fn, ms) {
const args = [];
for (let i = 2; i < arguments.length; ++i) {
args[i - 2] = arguments[i];
}
return startTimer(window, setInterval, clearInterval, ++latestTimerId, fn, ms, timers, args);
};
this.clearInterval = stopTimer.bind(this, timers);
this.clearTimeout = stopTimer.bind(this, timers);
if (this._pretendToBeVisual) {
this.requestAnimationFrame = fn => {
const timestamp = rawPerformance.now() - windowInitialized;
const fps = 1000 / 60;
return startTimer(
window,
setTimeout,
clearTimeout,
++latestAnimationFrameCallbackId,
fn,
fps,
animationFrameCallbacks,
[timestamp]
);
};
this.cancelAnimationFrame = stopTimer.bind(this, animationFrameCallbacks);
}
this.__stopAllTimers = function () {
stopAllTimers(timers);
stopAllTimers(animationFrameCallbacks);
latestTimerId = 0;
latestAnimationFrameCallbackId = 0;
timers = Object.create(null);
animationFrameCallbacks = Object.create(null);
};
function Option(text, value, defaultSelected, selected) {
if (text === undefined) {
text = "";
}
text = webIDLConversions.DOMString(text);
if (value !== undefined) {
value = webIDLConversions.DOMString(value);
}
defaultSelected = webIDLConversions.boolean(defaultSelected);
selected = webIDLConversions.boolean(selected);
const option = window._document.createElement("option");
const impl = idlUtils.implForWrapper(option);
if (text !== "") {
impl.text = text;
}
if (value !== undefined) {
impl.setAttribute("value", value);
}
if (defaultSelected) {
impl.setAttribute("selected", "");
}
impl._selectedness = selected;
return option;
}
Object.defineProperty(Option, "prototype", {
value: this.HTMLOptionElement.prototype,
configurable: false,
enumerable: false,
writable: false
});
Object.defineProperty(window, "Option", {
value: Option,
configurable: true,
enumerable: false,
writable: true
});
function Image() {
const img = window._document.createElement("img");
const impl = idlUtils.implForWrapper(img);
if (arguments.length > 0) {
impl.setAttribute("width", String(arguments[0]));
}
if (arguments.length > 1) {
impl.setAttribute("height", String(arguments[1]));
}
return img;
}
Object.defineProperty(Image, "prototype", {
value: this.HTMLImageElement.prototype,
configurable: false,
enumerable: false,
writable: false
});
Object.defineProperty(window, "Image", {
value: Image,
configurable: true,
enumerable: false,
writable: true
});
function Audio(src) {
const audio = window._document.createElement("audio");
const impl = idlUtils.implForWrapper(audio);
impl.setAttribute("preload", "auto");
if (src !== undefined) {
impl.setAttribute("src", String(src));
}
return audio;
}
Object.defineProperty(Audio, "prototype", {
value: this.HTMLAudioElement.prototype,
configurable: false,
enumerable: false,
writable: false
});
Object.defineProperty(window, "Audio", {
value: Audio,
configurable: true,
enumerable: false,
writable: true
});
function wrapConsoleMethod(method) {
return (...args) => {
window._virtualConsole.emit(method, ...args);
};
}
this.postMessage = postMessage;
this.atob = function (str) {
const result = atob(str);
if (result === null) {
throw new DOMException("The string to be decoded contains invalid characters.", "InvalidCharacterError");
}
return result;
};
this.btoa = function (str) {
const result = btoa(str);
if (result === null) {
throw new DOMException("The string to be encoded contains invalid characters.", "InvalidCharacterError");
}
return result;
};
this.FileReader = createFileReader({
window: this
}).interface;
this.WebSocket = createWebSocket({
window: this
}).interface;
const AbortSignalWrapper = createAbortSignal({
window: this
});
this.AbortSignal = AbortSignalWrapper.interface;
this.AbortController = createAbortController({
AbortSignal: AbortSignalWrapper
}).interface;
this.XMLHttpRequest = createXMLHttpRequest(this);
// TODO: necessary for Blob and FileReader due to different-globals weirdness; investigate how to avoid this.
this.ArrayBuffer = ArrayBuffer;
this.Int8Array = Int8Array;
this.Uint8Array = Uint8Array;
this.Uint8ClampedArray = Uint8ClampedArray;
this.Int16Array = Int16Array;
this.Uint16Array = Uint16Array;
this.Int32Array = Int32Array;
this.Uint32Array = Uint32Array;
this.Float32Array = Float32Array;
this.Float64Array = Float64Array;
this.stop = function () {
const manager = idlUtils.implForWrapper(this._document)._requestManager;
if (manager) {
manager.close();
}
};
this.close = function () {
// Recursively close child frame windows, then ourselves.
const currentWindow = this;
(function windowCleaner(windowToClean) {
for (let i = 0; i < windowToClean.length; i++) {
windowCleaner(windowToClean[i]);
}
// We"re already in our own window.close().
if (windowToClean !== currentWindow) {
windowToClean.close();
}
}(this));
// Clear out all listeners. Any in-flight or upcoming events should not get delivered.
idlUtils.implForWrapper(this)._eventListeners = Object.create(null);
if (this._document) {
if (this._document.body) {
this._document.body.innerHTML = "";
}
if (this._document.close) {
// It's especially important to clear out the listeners here because document.close() causes a "load" event to
// fire.
idlUtils.implForWrapper(this._document)._eventListeners = Object.create(null);
this._document.close();
}
const doc = idlUtils.implForWrapper(this._document);
if (doc._requestManager) {
doc._requestManager.close();
}
delete this._document;
}
this.__stopAllTimers();
WebSocketImpl.cleanUpWindow(this);
};
this.getComputedStyle = function (node) {
const nodeImpl = idlUtils.implForWrapper(node);
const s = node.style;
const cs = new CSSStyleDeclaration();
const { forEach } = Array.prototype;
function setPropertiesFromRule(rule) {
if (!rule.selectorText) {
return;
}
const selectors = rule.selectorText.split(cssSelectorSplitRE);
let matched = false;
for (const selectorText of selectors) {
if (selectorText !== "" && selectorText !== "," && !matched && matchesDontThrow(nodeImpl, selectorText)) {
matched = true;
forEach.call(rule.style, property => {
cs.setProperty(property, rule.style.getPropertyValue(property), rule.style.getPropertyPriority(property));
});
}
}
}
function readStylesFromStyleSheet(sheet) {
forEach.call(sheet.cssRules, rule => {
if (rule.media) {
if (Array.prototype.indexOf.call(rule.media, "screen") !== -1) {
forEach.call(rule.cssRules, setPropertiesFromRule);
}
} else {
setPropertiesFromRule(rule);
}
});
}
readStylesFromStyleSheet(defaultStyleSheet);
forEach.call(node.ownerDocument.styleSheets, readStylesFromStyleSheet);
forEach.call(s, property => {
cs.setProperty(property, s.getPropertyValue(property), s.getPropertyPriority(property));
});
return cs;
};
// The captureEvents() and releaseEvents() methods must do nothing
this.captureEvents = function () {};
this.releaseEvents = function () {};
///// PUBLIC DATA PROPERTIES (TODO: should be getters)
this.console = {
assert: wrapConsoleMethod("assert"),
clear: wrapConsoleMethod("clear"),
count: wrapConsoleMethod("count"),
debug: wrapConsoleMethod("debug"),
error: wrapConsoleMethod("error"),
group: wrapConsoleMethod("group"),
groupCollapsed: wrapConsoleMethod("groupCollapsed"),
groupEnd: wrapConsoleMethod("groupEnd"),
info: wrapConsoleMethod("info"),
log: wrapConsoleMethod("log"),
table: wrapConsoleMethod("table"),
time: wrapConsoleMethod("time"),
timeEnd: wrapConsoleMethod("timeEnd"),
trace: wrapConsoleMethod("trace"),
warn: wrapConsoleMethod("warn")
};
function notImplementedMethod(name) {
return function () {
notImplemented(name, window);
};
}
define(this, {
name: "nodejs",
// Node v6 has issues (presumably in the vm module)
// which this property exposes through an XHR test
// status: "",
devicePixelRatio: 1,
innerWidth: 1024,
innerHeight: 768,
outerWidth: 1024,
outerHeight: 768,
pageXOffset: 0,
pageYOffset: 0,
screenX: 0,
screenY: 0,
scrollX: 0,
scrollY: 0,
// Not in spec, but likely to be added eventually:
// https://github.com/w3c/csswg-drafts/issues/1091
screenLeft: 0,
screenTop: 0,
alert: notImplementedMethod("window.alert"),
blur: notImplementedMethod("window.blur"),
confirm: notImplementedMethod("window.confirm"),
focus: notImplementedMethod("window.focus"),
moveBy: notImplementedMethod("window.moveBy"),
moveTo: notImplementedMethod("window.moveTo"),
open: notImplementedMethod("window.open"),
print: notImplementedMethod("window.print"),
prompt: notImplementedMethod("window.prompt"),
resizeBy: notImplementedMethod("window.resizeBy"),
resizeTo: notImplementedMethod("window.resizeTo"),
scroll: notImplementedMethod("window.scroll"),
scrollBy: notImplementedMethod("window.scrollBy"),
scrollTo: notImplementedMethod("window.scrollTo")
});
///// INITIALIZATION
process.nextTick(() => {
if (!window.document) {
return; // window might've been closed already
}
if (window.document.readyState === "complete") {
const ev = window.document.createEvent("HTMLEvents");
ev.initEvent("load", false, false);
window.dispatchEvent(ev);
} else {
window.document.addEventListener("load", () => {
const ev = window.document.createEvent("HTMLEvents");
ev.initEvent("load", false, false);
window.dispatchEvent(ev);
});
}
});
}
Object.setPrototypeOf(Window, EventTarget.interface);
Object.setPrototypeOf(Window.prototype, EventTarget.interface.prototype);
Object.defineProperty(Window.prototype, Symbol.toStringTag, {
value: "Window",
writable: false,
enumerable: false,
configurable: true
});
function startTimer(window, startFn, stopFn, timerId, callback, ms, timerStorage, args) {
if (!window || !window._document) {
return undefined;
}
if (typeof callback !== "function") {
const code = String(callback);
callback = window._globalProxy.eval.bind(window, code + `\n//# sourceURL=${window.location.href}`);
}
const oldCallback = callback;
callback = () => {
try {
oldCallback.apply(window._globalProxy, args);
} catch (e) {
reportException(window, e, window.location.href);
}
};
const res = startFn(callback, ms);
timerStorage[timerId] = [res, stopFn];
return timerId;
}
function stopTimer(timerStorage, id) {
const timer = timerStorage[id];
if (timer) {
// Need to .call() with undefined to ensure the thisArg is not timer itself
timer[1].call(undefined, timer[0]);
delete timerStorage[id];
}
}
function stopAllTimers(timers) {
Object.keys(timers).forEach(key => {
const timer = timers[key];
// Need to .call() with undefined to ensure the thisArg is not timer itself
timer[1].call(undefined, timer[0]);
});
}

View file

@ -0,0 +1,785 @@
// Ideally, we would use
// https://html.spec.whatwg.org/multipage/rendering.html#the-css-user-agent-style-sheet-and-presentational-hints
// but for now, just use the version from blink. This file is copied from
// https://chromium.googlesource.com/chromium/blink/+/96aa3a280ab7d67178c8f122a04949ce5f8579e0/Source/core/css/html.css
// (removed a line which had octal literals inside since octal literals are not allowed in template strings)
// We use a .js file because otherwise we can't browserify this. (brfs is unusable due to lack of ES2015 support)
module.exports = `
/*
* The default style sheet used to render HTML.
*
* Copyright (C) 2000 Lars Knoll (knoll@kde.org)
* Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
@namespace "http://www.w3.org/1999/xhtml";
html {
display: block
}
:root {
scroll-blocks-on: start-touch wheel-event
}
/* children of the <head> element all have display:none */
head {
display: none
}
meta {
display: none
}
title {
display: none
}
link {
display: none
}
style {
display: none
}
script {
display: none
}
/* generic block-level elements */
body {
display: block;
margin: 8px
}
p {
display: block;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1__qem;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
}
div {
display: block
}
layer {
display: block
}
article, aside, footer, header, hgroup, main, nav, section {
display: block
}
marquee {
display: inline-block;
}
address {
display: block
}
blockquote {
display: block;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 40px;
-webkit-margin-end: 40px;
}
figcaption {
display: block
}
figure {
display: block;
-webkit-margin-before: 1em;
-webkit-margin-after: 1em;
-webkit-margin-start: 40px;
-webkit-margin-end: 40px;
}
q {
display: inline
}
/* nwmatcher does not support ::before and ::after, so we can't render q
correctly: https://html.spec.whatwg.org/multipage/rendering.html#phrasing-content-3
TODO: add q::before and q::after selectors
*/
center {
display: block;
/* special centering to be able to emulate the html4/netscape behaviour */
text-align: -webkit-center
}
hr {
display: block;
-webkit-margin-before: 0.5em;
-webkit-margin-after: 0.5em;
-webkit-margin-start: auto;
-webkit-margin-end: auto;
border-style: inset;
border-width: 1px;
box-sizing: border-box
}
map {
display: inline
}
video {
object-fit: contain;
}
/* heading elements */
h1 {
display: block;
font-size: 2em;
-webkit-margin-before: 0.67__qem;
-webkit-margin-after: 0.67em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
article h1,
aside h1,
nav h1,
section h1 {
font-size: 1.5em;
-webkit-margin-before: 0.83__qem;
-webkit-margin-after: 0.83em;
}
article article h1,
article aside h1,
article nav h1,
article section h1,
aside article h1,
aside aside h1,
aside nav h1,
aside section h1,
nav article h1,
nav aside h1,
nav nav h1,
nav section h1,
section article h1,
section aside h1,
section nav h1,
section section h1 {
font-size: 1.17em;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
}
/* Remaining selectors are deleted because nwmatcher does not support
:matches() and expanding the selectors manually would be far too verbose.
Also see https://html.spec.whatwg.org/multipage/rendering.html#sections-and-headings
TODO: rewrite to use :matches() when nwmatcher supports it.
*/
h2 {
display: block;
font-size: 1.5em;
-webkit-margin-before: 0.83__qem;
-webkit-margin-after: 0.83em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
h3 {
display: block;
font-size: 1.17em;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
h4 {
display: block;
-webkit-margin-before: 1.33__qem;
-webkit-margin-after: 1.33em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
h5 {
display: block;
font-size: .83em;
-webkit-margin-before: 1.67__qem;
-webkit-margin-after: 1.67em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
h6 {
display: block;
font-size: .67em;
-webkit-margin-before: 2.33__qem;
-webkit-margin-after: 2.33em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
/* tables */
table {
display: table;
border-collapse: separate;
border-spacing: 2px;
border-color: gray
}
thead {
display: table-header-group;
vertical-align: middle;
border-color: inherit
}
tbody {
display: table-row-group;
vertical-align: middle;
border-color: inherit
}
tfoot {
display: table-footer-group;
vertical-align: middle;
border-color: inherit
}
/* for tables without table section elements (can happen with XHTML or dynamically created tables) */
table > tr {
vertical-align: middle;
}
col {
display: table-column
}
colgroup {
display: table-column-group
}
tr {
display: table-row;
vertical-align: inherit;
border-color: inherit
}
td, th {
display: table-cell;
vertical-align: inherit
}
th {
font-weight: bold
}
caption {
display: table-caption;
text-align: -webkit-center
}
/* lists */
ul, menu, dir {
display: block;
list-style-type: disc;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
-webkit-padding-start: 40px
}
ol {
display: block;
list-style-type: decimal;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
-webkit-padding-start: 40px
}
li {
display: list-item;
text-align: -webkit-match-parent;
}
ul ul, ol ul {
list-style-type: circle
}
ol ol ul, ol ul ul, ul ol ul, ul ul ul {
list-style-type: square
}
dd {
display: block;
-webkit-margin-start: 40px
}
dl {
display: block;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
}
dt {
display: block
}
ol ul, ul ol, ul ul, ol ol {
-webkit-margin-before: 0;
-webkit-margin-after: 0
}
/* form elements */
form {
display: block;
margin-top: 0__qem;
}
label {
cursor: default;
}
legend {
display: block;
-webkit-padding-start: 2px;
-webkit-padding-end: 2px;
border: none
}
fieldset {
display: block;
-webkit-margin-start: 2px;
-webkit-margin-end: 2px;
-webkit-padding-before: 0.35em;
-webkit-padding-start: 0.75em;
-webkit-padding-end: 0.75em;
-webkit-padding-after: 0.625em;
border: 2px groove ThreeDFace;
min-width: -webkit-min-content;
}
button {
-webkit-appearance: button;
}
/* Form controls don't go vertical. */
input, textarea, select, button, meter, progress {
-webkit-writing-mode: horizontal-tb !important;
}
input, textarea, select, button {
margin: 0__qem;
font: -webkit-small-control;
text-rendering: auto; /* FIXME: Remove when tabs work with optimizeLegibility. */
color: initial;
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0;
text-shadow: none;
display: inline-block;
text-align: start;
}
/* TODO: Add " i" to attribute matchers to support case-insensitive matching */
input[type="hidden"] {
display: none
}
input {
-webkit-appearance: textfield;
padding: 1px;
background-color: white;
border: 2px inset;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
cursor: auto;
}
input[type="search"] {
-webkit-appearance: searchfield;
box-sizing: border-box;
}
select {
border-radius: 5px;
}
textarea {
-webkit-appearance: textarea;
background-color: white;
border: 1px solid;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
flex-direction: column;
resize: auto;
cursor: auto;
padding: 2px;
white-space: pre-wrap;
word-wrap: break-word;
}
input[type="password"] {
-webkit-text-security: disc !important;
}
input[type="hidden"], input[type="image"], input[type="file"] {
-webkit-appearance: initial;
padding: initial;
background-color: initial;
border: initial;
}
input[type="file"] {
align-items: baseline;
color: inherit;
text-align: start !important;
}
input[type="radio"], input[type="checkbox"] {
margin: 3px 0.5ex;
padding: initial;
background-color: initial;
border: initial;
}
input[type="button"], input[type="submit"], input[type="reset"] {
-webkit-appearance: push-button;
-webkit-user-select: none;
white-space: pre
}
input[type="button"], input[type="submit"], input[type="reset"], button {
align-items: flex-start;
text-align: center;
cursor: default;
color: ButtonText;
padding: 2px 6px 3px 6px;
border: 2px outset ButtonFace;
background-color: ButtonFace;
box-sizing: border-box
}
input[type="range"] {
-webkit-appearance: slider-horizontal;
padding: initial;
border: initial;
margin: 2px;
color: #909090;
}
input[type="button"]:disabled, input[type="submit"]:disabled, input[type="reset"]:disabled,
button:disabled, select:disabled, optgroup:disabled, option:disabled,
select[disabled]>option {
color: GrayText
}
input[type="button"]:active, input[type="submit"]:active, input[type="reset"]:active, button:active {
border-style: inset
}
input[type="button"]:active:disabled, input[type="submit"]:active:disabled, input[type="reset"]:active:disabled, button:active:disabled {
border-style: outset
}
datalist {
display: none
}
area {
display: inline;
cursor: pointer;
}
param {
display: none
}
input[type="checkbox"] {
-webkit-appearance: checkbox;
box-sizing: border-box;
}
input[type="radio"] {
-webkit-appearance: radio;
box-sizing: border-box;
}
input[type="color"] {
-webkit-appearance: square-button;
width: 44px;
height: 23px;
background-color: ButtonFace;
/* Same as native_theme_base. */
border: 1px #a9a9a9 solid;
padding: 1px 2px;
}
input[type="color"][list] {
-webkit-appearance: menulist;
width: 88px;
height: 23px
}
select {
-webkit-appearance: menulist;
box-sizing: border-box;
align-items: center;
border: 1px solid;
white-space: pre;
-webkit-rtl-ordering: logical;
color: black;
background-color: white;
cursor: default;
}
optgroup {
font-weight: bolder;
display: block;
}
option {
font-weight: normal;
display: block;
padding: 0 2px 1px 2px;
white-space: pre;
min-height: 1.2em;
}
output {
display: inline;
}
/* meter */
meter {
-webkit-appearance: meter;
box-sizing: border-box;
display: inline-block;
height: 1em;
width: 5em;
vertical-align: -0.2em;
}
/* progress */
progress {
-webkit-appearance: progress-bar;
box-sizing: border-box;
display: inline-block;
height: 1em;
width: 10em;
vertical-align: -0.2em;
}
/* inline elements */
u, ins {
text-decoration: underline
}
strong, b {
font-weight: bold
}
i, cite, em, var, address, dfn {
font-style: italic
}
tt, code, kbd, samp {
font-family: monospace
}
pre, xmp, plaintext, listing {
display: block;
font-family: monospace;
white-space: pre;
margin: 1__qem 0
}
mark {
background-color: yellow;
color: black
}
big {
font-size: larger
}
small {
font-size: smaller
}
s, strike, del {
text-decoration: line-through
}
sub {
vertical-align: sub;
font-size: smaller
}
sup {
vertical-align: super;
font-size: smaller
}
nobr {
white-space: nowrap
}
/* states */
:focus {
outline: auto 5px -webkit-focus-ring-color
}
/* Read-only text fields do not show a focus ring but do still receive focus */
html:focus, body:focus, input[readonly]:focus {
outline: none
}
embed:focus, iframe:focus, object:focus {
outline: none
}
input:focus, textarea:focus, select:focus {
outline-offset: -2px
}
input[type="button"]:focus,
input[type="checkbox"]:focus,
input[type="file"]:focus,
input[type="hidden"]:focus,
input[type="image"]:focus,
input[type="radio"]:focus,
input[type="reset"]:focus,
input[type="search"]:focus,
input[type="submit"]:focus {
outline-offset: 0
}
/* HTML5 ruby elements */
ruby, rt {
text-indent: 0; /* blocks used for ruby rendering should not trigger this */
}
rt {
line-height: normal;
-webkit-text-emphasis: none;
}
ruby > rt {
display: block;
font-size: 50%;
text-align: start;
}
ruby > rp {
display: none;
}
/* other elements */
noframes {
display: none
}
frameset, frame {
display: block
}
frameset {
border-color: inherit
}
iframe {
border: 2px inset
}
details {
display: block
}
summary {
display: block
}
template {
display: none
}
bdi, output {
unicode-bidi: -webkit-isolate;
}
bdo {
unicode-bidi: bidi-override;
}
textarea[dir=auto] {
unicode-bidi: -webkit-plaintext;
}
dialog:not([open]) {
display: none
}
dialog {
position: absolute;
left: 0;
right: 0;
width: -webkit-fit-content;
height: -webkit-fit-content;
margin: auto;
border: solid;
padding: 1em;
background: white;
color: black
}
/* noscript is handled internally, as it depends on settings. */
`;

View file

@ -0,0 +1,55 @@
"use strict";
const vm = require("vm");
const idlUtils = require("../living/generated/utils");
exports.availableDocumentFeatures = [
"FetchExternalResources",
"SkipExternalResources"
];
exports.defaultDocumentFeatures = {
FetchExternalResources: ["script", "link"], // omitted by default: "frame"
SkipExternalResources: false
};
exports.applyDocumentFeatures = (documentImpl, features = {}) => {
for (let i = 0; i < exports.availableDocumentFeatures.length; ++i) {
const featureName = exports.availableDocumentFeatures[i];
let featureSource;
if (features[featureName] !== undefined) {
featureSource = features[featureName];
// We have to check the lowercase version also because the Document feature
// methods convert everything to lowercase.
} else if (typeof features[featureName.toLowerCase()] !== "undefined") {
featureSource = features[featureName.toLowerCase()];
} else if (exports.defaultDocumentFeatures[featureName]) {
featureSource = exports.defaultDocumentFeatures[featureName];
} else {
continue;
}
const implImpl = documentImpl._implementation;
implImpl._removeFeature(featureName);
if (featureSource !== undefined) {
if (Array.isArray(featureSource)) {
for (let j = 0; j < featureSource.length; ++j) {
implImpl._addFeature(featureName, featureSource[j]);
}
} else {
implImpl._addFeature(featureName, featureSource);
}
}
}
};
exports.contextifyWindow = window => {
if (vm.isContext(window)) {
return;
}
vm.createContext(window);
const documentImpl = idlUtils.implForWrapper(window._document);
documentImpl._defaultView = window._globalProxy = vm.runInContext("this", window);
};

18
node_modules/jsdom/lib/jsdom/browser/domtohtml.js generated vendored Normal file
View file

@ -0,0 +1,18 @@
"use strict";
const parse5 = require("parse5");
const treeAdapter = require("./parse5-adapter-serialization");
const NODE_TYPE = require("../living/node-type");
exports.domToHtml = iterable => {
let ret = "";
for (const node of iterable) {
if (node.nodeType === NODE_TYPE.DOCUMENT_NODE) {
ret += parse5.serialize(node, { treeAdapter });
} else {
// TODO: maybe parse5 can give us a hook where it serializes the node itself too:
// https://github.com/inikulin/parse5/issues/230
ret += parse5.serialize({ childNodesForSerializing: [node] }, { treeAdapter });
}
}
return ret;
};

271
node_modules/jsdom/lib/jsdom/browser/htmltodom.js generated vendored Normal file
View file

@ -0,0 +1,271 @@
"use strict";
const parse5 = require("parse5");
const sax = require("sax");
const attributes = require("../living/attributes");
const DocumentType = require("../living/generated/DocumentType");
const JSDOMParse5Adapter = require("./parse5-adapter-parsing");
const { HTML_NS } = require("../living/helpers/namespaces");
// Horrible monkey-patch to implement https://github.com/inikulin/parse5/issues/237
const OpenElementStack = require("parse5/lib/parser/open_element_stack");
const originalPop = OpenElementStack.prototype.pop;
OpenElementStack.prototype.pop = function (...args) {
const before = this.items[this.stackTop];
originalPop.apply(this, args);
if (before._poppedOffStackOfOpenElements) {
before._poppedOffStackOfOpenElements();
}
};
const originalPush = OpenElementStack.prototype.push;
OpenElementStack.prototype.push = function (...args) {
originalPush.apply(this, args);
const after = this.items[this.stackTop];
if (after._pushedOnStackOfOpenElements) {
after._pushedOnStackOfOpenElements();
}
};
module.exports = class HTMLToDOM {
constructor(parsingMode) {
this.parser = parsingMode === "xml" ? sax : parse5;
}
appendToNode(html, node) {
html = String(html);
return this._doParse(html, true, node);
}
appendToDocument(html, documentImpl) {
html = String(html);
return this._doParse(html, false, documentImpl, documentImpl._parseOptions);
}
_doParse(...args) {
return this.parser === parse5 ? this._parseWithParse5(...args) : this._parseWithSax(...args);
}
_parseWithParse5(html, isFragment, contextNode, options = {}) {
const adapter = new JSDOMParse5Adapter(contextNode._ownerDocument || contextNode);
options.treeAdapter = adapter;
if (isFragment) {
const fragment = this.parser.parseFragment(contextNode, html, options);
if (contextNode._templateContents) {
contextNode._templateContents.appendChild(fragment);
} else {
contextNode.appendChild(fragment);
}
} else {
this.parser.parse(html, options);
}
return contextNode;
}
_parseWithSax(html, isFragment, contextNode) {
const SaxParser = this.parser.parser;
const parser = new SaxParser(/* strict = */true, { xmlns: true, strictEntities: true });
parser.noscript = false;
parser.looseCase = "toString";
const openStack = [contextNode];
parser.ontext = text => {
setChildForSax(openStack[openStack.length - 1], {
type: "text",
data: text
});
};
parser.oncdata = cdata => {
setChildForSax(openStack[openStack.length - 1], {
type: "cdata",
data: cdata
});
};
parser.onopentag = arg => {
const attrs = Object.keys(arg.attributes).map(key => {
const rawAttribute = arg.attributes[key];
let { prefix } = rawAttribute;
let localName = rawAttribute.local;
if (prefix === "xmlns" && localName === "") {
// intended weirdness in node-sax, see https://github.com/isaacs/sax-js/issues/165
localName = prefix;
prefix = null;
}
if (prefix === "") {
prefix = null;
}
const namespace = rawAttribute.uri === "" ? null : rawAttribute.uri;
return { name: rawAttribute.name, value: rawAttribute.value, prefix, localName, namespace };
});
const tag = {
type: "tag",
name: arg.local,
prefix: arg.prefix,
namespace: arg.uri,
attributes: attrs
};
if (arg.local === "script" && arg.uri === HTML_NS) {
openStack.push(tag);
} else {
const elem = setChildForSax(openStack[openStack.length - 1], tag);
openStack.push(elem);
}
};
parser.onclosetag = () => {
const elem = openStack.pop();
if (elem.constructor.name === "Object") { // we have an empty script tag
setChildForSax(openStack[openStack.length - 1], elem);
}
};
parser.onscript = scriptText => {
const tag = openStack.pop();
tag.children = [{ type: "text", data: scriptText }];
const elem = setChildForSax(openStack[openStack.length - 1], tag);
openStack.push(elem);
};
parser.oncomment = comment => {
setChildForSax(openStack[openStack.length - 1], {
type: "comment",
data: comment
});
};
parser.onprocessinginstruction = pi => {
setChildForSax(openStack[openStack.length - 1], {
type: "directive",
name: "?" + pi.name,
data: "?" + pi.name + " " + pi.body + "?"
});
};
parser.ondoctype = dt => {
setChildForSax(openStack[openStack.length - 1], {
type: "directive",
name: "!doctype",
data: "!doctype " + dt
});
const entityMatcher = /<!ENTITY ([^ ]+) "([^"]+)">/g;
let result;
while ((result = entityMatcher.exec(dt))) {
const [, name, value] = result;
if (!(name in parser.ENTITIES)) {
parser.ENTITIES[name] = value;
}
}
};
parser.onerror = err => {
throw err;
};
parser.write(html).close();
}
};
function setChildForSax(parentImpl, node) {
const currentDocument = (parentImpl && parentImpl._ownerDocument) || parentImpl;
let newNode;
let isTemplateContents = false;
switch (node.type) {
case "tag":
case "script":
case "style":
newNode = currentDocument._createElementWithCorrectElementInterface(node.name, node.namespace);
newNode._prefix = node.prefix || null;
newNode._namespaceURI = node.namespace || null;
break;
case "root":
// If we are in <template> then add all children to the parent's _templateContents; skip this virtual root node.
if (parentImpl.tagName === "TEMPLATE" && parentImpl._namespaceURI === HTML_NS) {
newNode = parentImpl._templateContents;
isTemplateContents = true;
}
break;
case "text":
// HTML entities should already be decoded by the parser, so no need to decode them
newNode = currentDocument.createTextNode(node.data);
break;
case "cdata":
newNode = currentDocument.createCDATASection(node.data);
break;
case "comment":
newNode = currentDocument.createComment(node.data);
break;
case "directive":
if (node.name[0] === "?" && node.name.toLowerCase() !== "?xml") {
const data = node.data.slice(node.name.length + 1, -1);
newNode = currentDocument.createProcessingInstruction(node.name.substring(1), data);
} else if (node.name.toLowerCase() === "!doctype") {
newNode = parseDocType(currentDocument, "<" + node.data + ">");
}
break;
}
if (!newNode) {
return null;
}
if (node.attributes) {
for (const a of node.attributes) {
attributes.setAttributeValue(newNode, a.localName, a.value, a.prefix, a.namespace);
}
}
if (node.children) {
for (let c = 0; c < node.children.length; c++) {
setChildForSax(newNode, node.children[c]);
}
}
if (!isTemplateContents) {
if (parentImpl._templateContents) {
// Setting innerHTML on a <template>
parentImpl._templateContents.appendChild(newNode);
} else {
parentImpl.appendChild(newNode);
}
}
return newNode;
}
const HTML5_DOCTYPE = /<!doctype html>/i;
const PUBLIC_DOCTYPE = /<!doctype\s+([^\s]+)\s+public\s+"([^"]+)"\s+"([^"]+)"/i;
const SYSTEM_DOCTYPE = /<!doctype\s+([^\s]+)\s+system\s+"([^"]+)"/i;
function parseDocType(doc, html) {
if (HTML5_DOCTYPE.test(html)) {
return createDocumentTypeInternal(doc, "html", "", "");
}
const publicPieces = PUBLIC_DOCTYPE.exec(html);
if (publicPieces) {
return createDocumentTypeInternal(doc, publicPieces[1], publicPieces[2], publicPieces[3]);
}
const systemPieces = SYSTEM_DOCTYPE.exec(html);
if (systemPieces) {
return createDocumentTypeInternal(doc, systemPieces[1], "", systemPieces[2]);
}
// Shouldn't get here (the parser shouldn't let us know about invalid doctypes), but our logic likely isn't
// real-world perfect, so let's fallback.
return createDocumentTypeInternal(doc, "html", "", "");
}
function createDocumentTypeInternal(ownerDocument, name, publicId, systemId) {
return DocumentType.createImpl([], { ownerDocument, name, publicId, systemId });
}

View file

@ -0,0 +1,13 @@
"use strict";
module.exports = function (nameForErrorMessage, window) {
if (!window) {
// Do nothing for window-less documents.
return;
}
const error = new Error(`Not implemented: ${nameForErrorMessage}`);
error.type = "not implemented";
window._virtualConsole.emit("jsdomError", error);
};

View file

@ -0,0 +1,110 @@
"use strict";
const DocumentType = require("../living/generated/DocumentType");
const DocumentFragment = require("../living/generated/DocumentFragment");
const Text = require("../living/generated/Text");
const Comment = require("../living/generated/Comment");
const attributes = require("../living/attributes");
const nodeTypes = require("../living/node-type");
const serializationAdapter = require("./parse5-adapter-serialization");
module.exports = class JSDOMParse5Adapter {
constructor(documentImpl) {
this._documentImpl = documentImpl;
}
createDocument() {
// parse5's model assumes that parse(html) will call into here to create the new Document, then return it. However,
// jsdom's model assumes we can create a Window (and through that create an empty Document), do some other setup
// stuff, and then parse, stuffing nodes into that Document as we go. So to adapt between these two models, we just
// return the already-created Document when asked by parse5 to "create" a Document.
return this._documentImpl;
}
createDocumentFragment() {
return DocumentFragment.createImpl([], { ownerDocument: this._documentImpl });
}
createElement(localName, namespace, attrs) {
const element = this._documentImpl._createElementWithCorrectElementInterface(localName, namespace);
element._namespaceURI = namespace;
this.adoptAttributes(element, attrs);
if ("_parserInserted" in element) {
element._parserInserted = true;
}
return element;
}
createCommentNode(data) {
return Comment.createImpl([], { data, ownerDocument: this._documentImpl });
}
appendChild(parentNode, newNode) {
parentNode.appendChild(newNode);
}
insertBefore(parentNode, newNode, referenceNode) {
parentNode.insertBefore(newNode, referenceNode);
}
setTemplateContent(templateElement, contentFragment) {
templateElement._templateContents = contentFragment;
}
setDocumentType(document, name, publicId, systemId) {
// parse5 sometimes gives us these as null.
if (name === null) {
name = "";
}
if (publicId === null) {
publicId = "";
}
if (systemId === null) {
systemId = "";
}
const documentType = DocumentType.createImpl([], { name, publicId, systemId, ownerDocument: this._documentImpl });
document.appendChild(documentType);
}
setDocumentMode(document, mode) {
// TODO: the rest of jsdom ignores this
document._mode = mode;
}
detachNode(node) {
node.remove();
}
insertText(parentNode, text) {
const { lastChild } = parentNode;
if (lastChild && lastChild.nodeType === nodeTypes.TEXT_NODE) {
lastChild.data += text;
} else {
const textNode = Text.createImpl([], { data: text, ownerDocument: this._documentImpl });
parentNode.appendChild(textNode);
}
}
insertTextBefore(parentNode, text, referenceNode) {
const { previousSibling } = referenceNode;
if (previousSibling && previousSibling.nodeType === nodeTypes.TEXT_NODE) {
previousSibling.data += text;
} else {
const textNode = Text.createImpl([], { data: text, ownerDocument: this._documentImpl });
parentNode.insertBefore(textNode, referenceNode);
}
}
adoptAttributes(element, attrs) {
for (const attr of attrs) {
const prefix = attr.prefix === "" ? null : attr.prefix;
attributes.setAttributeValue(element, attr.name, attr.value, prefix, attr.namespace);
}
}
};
Object.assign(module.exports.prototype, serializationAdapter);

View file

@ -0,0 +1,41 @@
"use strict";
const idlUtils = require("../living/generated/utils");
const nodeTypes = require("../living/node-type");
const { domSymbolTree } = require("../living/helpers/internal-constants");
// Serialization only requires a subset of the tree adapter interface.
// Tree traversing
exports.getFirstChild = node => node.firstChild;
exports.getChildNodes = node => node.childNodesForSerializing || domSymbolTree.childrenToArray(node);
exports.getParentNode = node => node.parentNode;
exports.getAttrList = node => idlUtils.wrapperForImpl(node._attributes);
// Node data
exports.getTagName = element => element._qualifiedName; // https://github.com/inikulin/parse5/issues/231
exports.getNamespaceURI = element => element.namespaceURI;
exports.getTextNodeContent = exports.getCommentNodeContent = node => node.data;
exports.getDocumentTypeNodeName = node => node.name;
exports.getDocumentTypeNodePublicId = node => node.publicId;
exports.getDocumentTypeNodeSystemId = node => node.systemId;
exports.getTemplateContent = templateElement => templateElement._templateContents;
exports.getDocumentMode = document => document._mode;
// Node types
exports.isTextNode = node => node.nodeType === nodeTypes.TEXT_NODE;
exports.isCommentNode = node => node.nodeType === nodeTypes.COMMENT_NODE;
exports.isDocumentTypeNode = node => node.nodeType === nodeTypes.DOCUMENT_TYPE_NODE;
exports.isElementNode = node => node.nodeType === nodeTypes.ELEMENT_NODE;

275
node_modules/jsdom/lib/jsdom/browser/resource-loader.js generated vendored Normal file
View file

@ -0,0 +1,275 @@
"use strict";
const MIMEType = require("whatwg-mimetype");
const parseDataURL = require("data-urls");
const sniffHTMLEncoding = require("html-encoding-sniffer");
const whatwgEncoding = require("whatwg-encoding");
const fs = require("fs");
const request = require("request");
const { documentBaseURLSerialized } = require("../living/helpers/document-base-url");
const NODE_TYPE = require("../living/node-type");
/* eslint-disable no-restricted-modules */
// TODO: stop using the built-in URL in favor of the spec-compliant whatwg-url package
// This legacy usage is in the process of being purged.
const URL = require("url");
/* eslint-enable no-restricted-modules */
const IS_BROWSER = Object.prototype.toString.call(process) !== "[object process]";
function createResourceLoadHandler(element, resourceUrl, document, loadCallback) {
if (loadCallback === undefined) {
loadCallback = () => {
// do nothing
};
}
return (err, data, response) => {
const ev = document.createEvent("HTMLEvents");
if (!err) {
try {
loadCallback.call(element, data, resourceUrl, response);
ev.initEvent("load", false, false);
} catch (e) {
err = e;
}
}
if (err) {
if (!err.isAbortError) {
ev.initEvent("error", false, false);
ev.error = err;
element.dispatchEvent(ev);
const error = new Error(`Could not load ${element.localName}: "${resourceUrl}"`);
error.detail = err;
error.type = "resource loading";
document._defaultView._virtualConsole.emit("jsdomError", error);
}
} else {
element.dispatchEvent(ev);
}
};
}
exports.readFile = function (filePath, { defaultEncoding, detectMetaCharset }, callback) {
const readableStream = fs.createReadStream(filePath);
let data = Buffer.alloc(0);
readableStream.on("error", callback);
readableStream.on("data", chunk => {
data = Buffer.concat([data, chunk]);
});
readableStream.on("end", () => {
// Not passing default encoding means binary
if (defaultEncoding) {
const encoding = detectMetaCharset ?
sniffHTMLEncoding(data, { defaultEncoding }) :
whatwgEncoding.getBOMEncoding(data) || defaultEncoding;
const decoded = whatwgEncoding.decode(data, encoding);
callback(null, decoded, { headers: { "content-type": "text/plain;charset=" + encoding } });
} else {
callback(null, data);
}
});
return {
abort() {
readableStream.destroy();
const error = new Error("request canceled by user");
error.isAbortError = true;
callback(error);
}
};
};
function readDataURL(dataURL, { defaultEncoding, detectMetaCharset }, callback) {
try {
const parsed = parseDataURL(dataURL);
// If default encoding does not exist, pass on binary data.
if (defaultEncoding) {
const sniffOptions = {
transportLayerEncodingLabel: parsed.mimeType.parameters.get("charset"),
defaultEncoding
};
const encoding = detectMetaCharset ?
sniffHTMLEncoding(parsed.body, sniffOptions) :
whatwgEncoding.getBOMEncoding(parsed.body) ||
whatwgEncoding.labelToName(parsed.mimeType.parameters.get("charset")) ||
defaultEncoding;
const decoded = whatwgEncoding.decode(parsed.body, encoding);
parsed.mimeType.parameters.set("charset", encoding);
callback(null, decoded, { headers: { "content-type": parsed.mimeType.toString() } });
} else {
callback(null, parsed.body, { headers: { "content-type": parsed.mimeType.toString() } });
}
} catch (err) {
callback(err, null);
}
return null;
}
// NOTE: request wraps tough-cookie cookie jar
// (see: https://github.com/request/request/blob/master/lib/cookies.js).
// Therefore, to pass our cookie jar to the request, we need to create
// request's wrapper and monkey patch it with our jar.
exports.wrapCookieJarForRequest = cookieJar => {
const jarWrapper = request.jar();
jarWrapper._jar = cookieJar;
return jarWrapper;
};
function fetch(urlObj, options, callback) {
if (urlObj.protocol === "data:") {
return readDataURL(urlObj.href, options, callback);
} else if (urlObj.hostname) {
return exports.download(urlObj, options, callback);
}
const filePath = urlObj.pathname
.replace(/^file:\/\//, "")
.replace(/^\/([a-z]):\//i, "$1:/")
.replace(/%20/g, " ");
return exports.readFile(filePath, options, callback);
}
exports.enqueue = function (element, resourceUrl, callback) {
const document = element.nodeType === NODE_TYPE.DOCUMENT_NODE ? element : element._ownerDocument;
if (document._queue) {
const loadHandler = createResourceLoadHandler(element, resourceUrl || document.URL, document, callback);
return document._queue.push(loadHandler);
}
return () => {
// do nothing in queue-less documents
};
};
exports.download = function (url, options, callback) {
const requestOptions = {
pool: options.pool,
agent: options.agent,
agentOptions: options.agentOptions,
agentClass: options.agentClass,
strictSSL: options.strictSSL,
gzip: true,
jar: exports.wrapCookieJarForRequest(options.cookieJar),
encoding: null,
headers: {
"User-Agent": options.userAgent,
"Accept-Language": "en",
Accept: options.accept || "*/*"
}
};
if (options.referrer && !IS_BROWSER) {
requestOptions.headers.referer = options.referrer;
}
if (options.proxy) {
requestOptions.proxy = options.proxy;
}
Object.assign(requestOptions.headers, options.headers);
const { defaultEncoding, detectMetaCharset } = options;
const req = request(url, requestOptions, (error, response, bufferData) => {
if (!error) {
// If default encoding does not exist, pass on binary data.
if (defaultEncoding) {
const contentType = MIMEType.parse(response.headers["content-type"]) || new MIMEType("text/plain");
const sniffOptions = {
transportLayerEncodingLabel: contentType.parameters.get("charset"),
defaultEncoding
};
const encoding = detectMetaCharset ?
sniffHTMLEncoding(bufferData, sniffOptions) :
whatwgEncoding.getBOMEncoding(bufferData) ||
whatwgEncoding.labelToName(contentType.parameters.get("charset")) ||
defaultEncoding;
const decoded = whatwgEncoding.decode(bufferData, encoding);
contentType.parameters.set("charset", encoding);
response.headers["content-type"] = contentType.toString();
callback(null, decoded, response);
} else {
callback(null, bufferData, response);
}
} else {
callback(error, null, response);
}
});
return {
abort() {
req.abort();
const error = new Error("request canceled by user");
error.isAbortError = true;
callback(error);
}
};
};
exports.load = function (element, urlString, options, callback) {
const document = element._ownerDocument;
const documentImpl = document.implementation;
if (!documentImpl._hasFeature("FetchExternalResources", element.tagName.toLowerCase())) {
return;
}
if (documentImpl._hasFeature("SkipExternalResources", urlString)) {
return;
}
const urlObj = URL.parse(urlString);
const enqueued = exports.enqueue(element, urlString, callback);
const customLoader = document._customResourceLoader;
const requestManager = document._requestManager;
const cookieJar = document._cookieJar;
options.accept = element._accept;
options.cookieJar = cookieJar;
options.referrer = document.URL;
options.pool = document._pool;
options.agentOptions = document._agentOptions;
options.strictSSL = document._strictSSL;
options.proxy = document._proxy;
options.userAgent = document._defaultView.navigator.userAgent;
let req = null;
function wrappedEnqueued() {
if (req && requestManager) {
requestManager.remove(req);
}
// do not trigger if the window is closed
if (element._ownerDocument && element._ownerDocument.defaultView.document) {
enqueued.apply(this, arguments);
}
}
if (typeof customLoader === "function") {
req = customLoader(
{
element,
url: urlObj,
cookie: cookieJar.getCookieStringSync(urlObj, { http: true }),
baseUrl: documentBaseURLSerialized(document),
defaultFetch(fetchCallback) {
return fetch(urlObj, options, fetchCallback);
}
},
wrappedEnqueued
);
} else {
req = fetch(urlObj, options, wrappedEnqueued);
}
if (req && requestManager) {
requestManager.add(req);
}
};