Spaces:
Running
Running
| ; | |
| const path = require("path"); | |
| const fs = require("fs").promises; | |
| const vm = require("vm"); | |
| const toughCookie = require("tough-cookie"); | |
| const sniffHTMLEncoding = require("html-encoding-sniffer"); | |
| const whatwgURL = require("whatwg-url"); | |
| const whatwgEncoding = require("whatwg-encoding"); | |
| const { URL } = require("whatwg-url"); | |
| const MIMEType = require("whatwg-mimetype"); | |
| const idlUtils = require("./jsdom/living/generated/utils.js"); | |
| const VirtualConsole = require("./jsdom/virtual-console.js"); | |
| const { createWindow } = require("./jsdom/browser/Window.js"); | |
| const { parseIntoDocument } = require("./jsdom/browser/parser"); | |
| const { fragmentSerialization } = require("./jsdom/living/domparsing/serialization.js"); | |
| const ResourceLoader = require("./jsdom/browser/resources/resource-loader.js"); | |
| const NoOpResourceLoader = require("./jsdom/browser/resources/no-op-resource-loader.js"); | |
| class CookieJar extends toughCookie.CookieJar { | |
| constructor(store, options) { | |
| // jsdom cookie jars must be loose by default | |
| super(store, { looseMode: true, ...options }); | |
| } | |
| } | |
| const window = Symbol("window"); | |
| let sharedFragmentDocument = null; | |
| class JSDOM { | |
| constructor(input = "", options = {}) { | |
| const mimeType = new MIMEType(options.contentType === undefined ? "text/html" : options.contentType); | |
| const { html, encoding } = normalizeHTML(input, mimeType); | |
| options = transformOptions(options, encoding, mimeType); | |
| this[window] = createWindow(options.windowOptions); | |
| const documentImpl = idlUtils.implForWrapper(this[window]._document); | |
| options.beforeParse(this[window]._globalProxy); | |
| parseIntoDocument(html, documentImpl); | |
| documentImpl.close(); | |
| } | |
| get window() { | |
| // It's important to grab the global proxy, instead of just the result of `createWindow(...)`, since otherwise | |
| // things like `window.eval` don't exist. | |
| return this[window]._globalProxy; | |
| } | |
| get virtualConsole() { | |
| return this[window]._virtualConsole; | |
| } | |
| get cookieJar() { | |
| // TODO NEWAPI move _cookieJar to window probably | |
| return idlUtils.implForWrapper(this[window]._document)._cookieJar; | |
| } | |
| serialize() { | |
| return fragmentSerialization(idlUtils.implForWrapper(this[window]._document), { requireWellFormed: false }); | |
| } | |
| nodeLocation(node) { | |
| if (!idlUtils.implForWrapper(this[window]._document)._parseOptions.sourceCodeLocationInfo) { | |
| throw new Error("Location information was not saved for this jsdom. Use includeNodeLocations during creation."); | |
| } | |
| return idlUtils.implForWrapper(node).sourceCodeLocation; | |
| } | |
| getInternalVMContext() { | |
| if (!vm.isContext(this[window])) { | |
| throw new TypeError("This jsdom was not configured to allow script running. " + | |
| "Use the runScripts option during creation."); | |
| } | |
| return this[window]; | |
| } | |
| reconfigure(settings) { | |
| if ("windowTop" in settings) { | |
| this[window]._top = settings.windowTop; | |
| } | |
| if ("url" in settings) { | |
| const document = idlUtils.implForWrapper(this[window]._document); | |
| const url = whatwgURL.parseURL(settings.url); | |
| if (url === null) { | |
| throw new TypeError(`Could not parse "${settings.url}" as a URL`); | |
| } | |
| document._URL = url; | |
| document._origin = whatwgURL.serializeURLOrigin(document._URL); | |
| this[window]._sessionHistory.currentEntry.url = url; | |
| } | |
| } | |
| static fragment(string = "") { | |
| if (!sharedFragmentDocument) { | |
| sharedFragmentDocument = (new JSDOM()).window.document; | |
| } | |
| const template = sharedFragmentDocument.createElement("template"); | |
| template.innerHTML = string; | |
| return template.content; | |
| } | |
| static fromURL(url, options = {}) { | |
| return Promise.resolve().then(() => { | |
| // Remove the hash while sending this through the research loader fetch(). | |
| // It gets added back a few lines down when constructing the JSDOM object. | |
| const parsedURL = new URL(url); | |
| const originalHash = parsedURL.hash; | |
| parsedURL.hash = ""; | |
| url = parsedURL.href; | |
| options = normalizeFromURLOptions(options); | |
| const resourceLoader = resourcesToResourceLoader(options.resources); | |
| const resourceLoaderForInitialRequest = resourceLoader.constructor === NoOpResourceLoader ? | |
| new ResourceLoader() : | |
| resourceLoader; | |
| const req = resourceLoaderForInitialRequest.fetch(url, { | |
| accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", | |
| cookieJar: options.cookieJar, | |
| referrer: options.referrer | |
| }); | |
| return req.then(body => { | |
| const res = req.response; | |
| options = Object.assign(options, { | |
| url: req.href + originalHash, | |
| contentType: res.headers["content-type"], | |
| referrer: req.getHeader("referer") ?? undefined | |
| }); | |
| return new JSDOM(body, options); | |
| }); | |
| }); | |
| } | |
| static async fromFile(filename, options = {}) { | |
| options = normalizeFromFileOptions(filename, options); | |
| const buffer = await fs.readFile(filename); | |
| return new JSDOM(buffer, options); | |
| } | |
| } | |
| function normalizeFromURLOptions(options) { | |
| // Checks on options that are invalid for `fromURL` | |
| if (options.url !== undefined) { | |
| throw new TypeError("Cannot supply a url option when using fromURL"); | |
| } | |
| if (options.contentType !== undefined) { | |
| throw new TypeError("Cannot supply a contentType option when using fromURL"); | |
| } | |
| // Normalization of options which must be done before the rest of the fromURL code can use them, because they are | |
| // given to request() | |
| const normalized = { ...options }; | |
| if (options.referrer !== undefined) { | |
| normalized.referrer = (new URL(options.referrer)).href; | |
| } | |
| if (options.cookieJar === undefined) { | |
| normalized.cookieJar = new CookieJar(); | |
| } | |
| return normalized; | |
| // All other options don't need to be processed yet, and can be taken care of in the normal course of things when | |
| // `fromURL` calls `new JSDOM(html, options)`. | |
| } | |
| function normalizeFromFileOptions(filename, options) { | |
| const normalized = { ...options }; | |
| if (normalized.contentType === undefined) { | |
| const extname = path.extname(filename); | |
| if (extname === ".xhtml" || extname === ".xht" || extname === ".xml") { | |
| normalized.contentType = "application/xhtml+xml"; | |
| } | |
| } | |
| if (normalized.url === undefined) { | |
| normalized.url = new URL("file:" + path.resolve(filename)); | |
| } | |
| return normalized; | |
| } | |
| function transformOptions(options, encoding, mimeType) { | |
| const transformed = { | |
| windowOptions: { | |
| // Defaults | |
| url: "about:blank", | |
| referrer: "", | |
| contentType: "text/html", | |
| parsingMode: "html", | |
| parseOptions: { | |
| sourceCodeLocationInfo: false, | |
| scriptingEnabled: false | |
| }, | |
| runScripts: undefined, | |
| encoding, | |
| pretendToBeVisual: false, | |
| storageQuota: 5000000, | |
| // Defaults filled in later | |
| resourceLoader: undefined, | |
| virtualConsole: undefined, | |
| cookieJar: undefined | |
| }, | |
| // Defaults | |
| beforeParse() { } | |
| }; | |
| // options.contentType was parsed into mimeType by the caller. | |
| if (!mimeType.isHTML() && !mimeType.isXML()) { | |
| throw new RangeError(`The given content type of "${options.contentType}" was not a HTML or XML content type`); | |
| } | |
| transformed.windowOptions.contentType = mimeType.essence; | |
| transformed.windowOptions.parsingMode = mimeType.isHTML() ? "html" : "xml"; | |
| if (options.url !== undefined) { | |
| transformed.windowOptions.url = (new URL(options.url)).href; | |
| } | |
| if (options.referrer !== undefined) { | |
| transformed.windowOptions.referrer = (new URL(options.referrer)).href; | |
| } | |
| if (options.includeNodeLocations) { | |
| if (transformed.windowOptions.parsingMode === "xml") { | |
| throw new TypeError("Cannot set includeNodeLocations to true with an XML content type"); | |
| } | |
| transformed.windowOptions.parseOptions = { sourceCodeLocationInfo: true }; | |
| } | |
| transformed.windowOptions.cookieJar = options.cookieJar === undefined ? | |
| new CookieJar() : | |
| options.cookieJar; | |
| transformed.windowOptions.virtualConsole = options.virtualConsole === undefined ? | |
| (new VirtualConsole()).sendTo(console) : | |
| options.virtualConsole; | |
| if (!(transformed.windowOptions.virtualConsole instanceof VirtualConsole)) { | |
| throw new TypeError("virtualConsole must be an instance of VirtualConsole"); | |
| } | |
| transformed.windowOptions.resourceLoader = resourcesToResourceLoader(options.resources); | |
| if (options.runScripts !== undefined) { | |
| transformed.windowOptions.runScripts = String(options.runScripts); | |
| if (transformed.windowOptions.runScripts === "dangerously") { | |
| transformed.windowOptions.parseOptions.scriptingEnabled = true; | |
| } else if (transformed.windowOptions.runScripts !== "outside-only") { | |
| throw new RangeError(`runScripts must be undefined, "dangerously", or "outside-only"`); | |
| } | |
| } | |
| if (options.beforeParse !== undefined) { | |
| transformed.beforeParse = options.beforeParse; | |
| } | |
| if (options.pretendToBeVisual !== undefined) { | |
| transformed.windowOptions.pretendToBeVisual = Boolean(options.pretendToBeVisual); | |
| } | |
| if (options.storageQuota !== undefined) { | |
| transformed.windowOptions.storageQuota = Number(options.storageQuota); | |
| } | |
| return transformed; | |
| } | |
| function normalizeHTML(html, mimeType) { | |
| let encoding = "UTF-8"; | |
| if (ArrayBuffer.isView(html)) { | |
| html = Buffer.from(html.buffer, html.byteOffset, html.byteLength); | |
| } else if (html instanceof ArrayBuffer) { | |
| html = Buffer.from(html); | |
| } | |
| if (Buffer.isBuffer(html)) { | |
| encoding = sniffHTMLEncoding(html, { | |
| defaultEncoding: mimeType.isXML() ? "UTF-8" : "windows-1252", | |
| transportLayerEncodingLabel: mimeType.parameters.get("charset") | |
| }); | |
| html = whatwgEncoding.decode(html, encoding); | |
| } else { | |
| html = String(html); | |
| } | |
| return { html, encoding }; | |
| } | |
| function resourcesToResourceLoader(resources) { | |
| switch (resources) { | |
| case undefined: { | |
| return new NoOpResourceLoader(); | |
| } | |
| case "usable": { | |
| return new ResourceLoader(); | |
| } | |
| default: { | |
| if (!(resources instanceof ResourceLoader)) { | |
| throw new TypeError("resources must be an instance of ResourceLoader"); | |
| } | |
| return resources; | |
| } | |
| } | |
| } | |
| exports.JSDOM = JSDOM; | |
| exports.VirtualConsole = VirtualConsole; | |
| exports.CookieJar = CookieJar; | |
| exports.ResourceLoader = ResourceLoader; | |
| exports.toughCookie = toughCookie; | |