jsdom
Version:
A JavaScript implementation of many web standards
299 lines (245 loc) • 9.08 kB
JavaScript
"use strict";
const vm = require("node:vm");
const { getBOMEncoding, labelToName, legacyHookDecode } = require("@exodus/bytes/encoding.js");
const { MIMEType } = require("whatwg-mimetype");
const { serializeURL } = require("whatwg-url");
const HTMLElementImpl = require("./HTMLElement-impl").implementation;
const reportException = require("../helpers/runtime-script-errors");
const { asciiLowercase, stripLeadingAndTrailingASCIIWhitespace } = require("../helpers/strings");
const { childTextContent } = require("../helpers/text");
const { isShadowRoot } = require("../helpers/shadow-dom");
const nodeTypes = require("../node-type");
const jsMIMETypes = new Set([
"application/ecmascript",
"application/javascript",
"application/x-ecmascript",
"application/x-javascript",
"text/ecmascript",
"text/javascript",
"text/javascript1.0",
"text/javascript1.1",
"text/javascript1.2",
"text/javascript1.3",
"text/javascript1.4",
"text/javascript1.5",
"text/jscript",
"text/livescript",
"text/x-ecmascript",
"text/x-javascript"
]);
class HTMLScriptElementImpl extends HTMLElementImpl {
#alreadyStarted = false;
constructor(globalObject, args, privateData) {
super(globalObject, args, privateData);
this._parserInserted = false; // set by the parser
}
_postConnectionSteps() {
// In our current terribly-hacky document.write() implementation, we parse in a div them move elements into the main
// document. Thus _eval() will bail early when it gets in _poppedOffStackOfOpenElements(), since we're not attached
// then. Instead, we'll let it eval here.
if (!this._parserInserted || this._isMovingDueToDocumentWrite) {
this._eval();
}
}
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model, moved from the
// children changed steps to the children inserted steps in
// https://github.com/whatwg/html/pull/12282. Being inserted-only is what keeps a removal or a
// character data change from running the script.
_childrenInsertedSteps() {
super._childrenInsertedSteps();
if (!this.isConnected) {
return;
}
this._postConnectionSteps();
}
_canRunScript() {
const document = this._ownerDocument;
// Reject destroyed documents; this is not a complete check for whether the document is fully active.
if (document._isDestroyed) {
return false;
}
// Equivalent to the spec's "scripting is disabled" check.
if (!document._defaultView || document._defaultView._settings.runScripts !== "dangerously" ||
document._scriptingDisabled) {
return false;
}
return true;
}
_fetchExternalScript() {
const document = this._ownerDocument;
const resourceLoader = document._resourceLoader;
const defaultEncoding = labelToName(this.getAttributeNS(null, "charset")) || document._encoding;
if (!this._canRunScript()) {
return;
}
const src = this.getAttributeNS(null, "src");
const url = this._ownerDocument.encodingParseAURL(src);
if (url === null) {
return;
}
const urlString = serializeURL(url);
const onLoadExternalScript = (data, response) => {
if (!response.ok) {
throw new Error("Status code: " + response.status);
}
const contentType = MIMEType.parse(response.headers.get("content-type")) || new MIMEType("text/plain");
const encoding = labelToName(getBOMEncoding(data)) ||
labelToName(contentType.parameters.get("charset")) ||
defaultEncoding;
const script = legacyHookDecode(data, encoding);
this._innerEval(script, urlString);
};
resourceLoader.fetch(urlString, {
element: this,
onLoad: onLoadExternalScript
});
}
_fetchInternalScript() {
const document = this._ownerDocument;
if (!this._canRunScript()) {
return;
}
if (!this._parserInserted) {
this._innerEval(this.text, document.URL);
return;
}
document._queue.push(null, () => {
this._innerEval(this.text, document.URL);
}, null, false, this);
}
_attributeChangeSteps(localName, oldValue, value, namespace) {
super._attributeChangeSteps(localName, oldValue, value, namespace);
if (namespace === null && localName === "src" && value !== null && this.isConnected) {
this._postConnectionSteps();
}
}
_poppedOffStackOfOpenElements() {
// This seems to roughly correspond to
// https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-incdata:prepare-a-script, although we certainly
// don't implement the full semantics.
// Both parsers also call this while parsing fragments. Until fragment parsing marks scripts as already started,
// do not prepare disconnected scripts here: that would clear `_parserInserted` and make `innerHTML` scripts run.
// This also preserves `_parserInserted` for the temporary fragment used by `document.write()`.
if (this.isConnected) {
this._eval();
}
}
// Vaguely similar to https://html.spec.whatwg.org/multipage/scripting.html#prepare-a-script, but we have a long way
// to go before it's aligned.
_eval() {
if (this.#alreadyStarted) {
return;
}
const wasParserInserted = this._parserInserted;
this._parserInserted = false;
if (!this.hasAttributeNS(null, "src") && this.text.length === 0) {
return;
}
if (!this.isConnected) {
return;
}
const scriptBlocksTypeString = this._getTypeString();
const type = getType(scriptBlocksTypeString);
if (type !== "classic") {
// TODO: implement modules, and then change the check to `type === null`.
return;
}
this._parserInserted = wasParserInserted;
this.#alreadyStarted = true;
// TODO: implement nomodule here, **but only after we support modules**.
// https://html.spec.whatwg.org/multipage/scripting.html#prepare-the-script-element
if (this.hasAttributeNS(null, "event") && this.hasAttributeNS(null, "for")) {
const forValue = asciiLowercase(stripLeadingAndTrailingASCIIWhitespace(this.getAttributeNS(null, "for")));
const event = asciiLowercase(stripLeadingAndTrailingASCIIWhitespace(this.getAttributeNS(null, "event")));
if (forValue !== "window" || (event !== "onload" && event !== "onload()")) {
return;
}
}
// At this point we completely depart from the spec.
if (this.hasAttributeNS(null, "src")) {
this._fetchExternalScript();
} else {
this._fetchInternalScript();
}
}
_innerEval(text, filename) {
if (this._ownerDocument._isDestroyed) {
return;
}
this._ownerDocument._writeAfterElement = this;
processJavaScript(this, text, filename);
delete this._ownerDocument._writeAfterElement;
}
_getTypeString() {
const typeAttr = this.getAttributeNS(null, "type");
const langAttr = this.getAttributeNS(null, "language");
if (typeAttr === "") {
return "text/javascript";
}
if (typeAttr === null && langAttr === "") {
return "text/javascript";
}
if (typeAttr === null && langAttr === null) {
return "text/javascript";
}
if (typeAttr !== null) {
return stripLeadingAndTrailingASCIIWhitespace(typeAttr);
}
if (langAttr !== null) {
return "text/" + langAttr;
}
return null;
}
get text() {
return childTextContent(this);
}
set text(text) {
this.textContent = text;
}
// https://html.spec.whatwg.org/multipage/scripting.html#script-processing-model
_cloningSteps(copy) {
copy.#alreadyStarted = this.#alreadyStarted;
}
}
function processJavaScript(element, code, filename) {
const document = element.ownerDocument;
const window = document._defaultView;
if (window) {
// https://html.spec.whatwg.org/multipage/scripting.html#execute-the-script-element
const oldCurrentScript = document._currentScript;
document._currentScript = isShadowRoot(element.getRootNode()) ? null : element;
let lineOffset = 0;
if (!element.hasAttributeNS(null, "src")) {
for (const child of element._children()) {
if (child.nodeType === nodeTypes.TEXT_NODE) {
if (child.sourceCodeLocation) {
lineOffset = child.sourceCodeLocation.startLine - 1;
}
break;
}
}
}
try {
vm.runInContext(code, window._globalObject, { filename, lineOffset, displayErrors: false });
} catch (e) {
reportException(window, e, filename);
} finally {
document._currentScript = oldCurrentScript;
}
}
}
function getType(typeString) {
const lowercased = asciiLowercase(typeString);
// Cannot use whatwg-mimetype parsing because that strips whitespace. The spec demands a strict string comparison.
// That is, the type="" attribute is not really related to MIME types at all.
if (jsMIMETypes.has(lowercased)) {
return "classic";
}
if (lowercased === "module") {
return "module";
}
return null;
}
module.exports = {
implementation: HTMLScriptElementImpl
};