jsdom
Version:
A JavaScript implementation of many web standards
203 lines (160 loc) • 6.99 kB
JavaScript
const { SaxesParser } = require("saxes");
const DOMException = require("../../living/generated/DOMException");
const { createElement } = require("../../living/helpers/create-element");
const DocumentFragment = require("../../living/generated/DocumentFragment");
const DocumentType = require("../../living/generated/DocumentType");
const CDATASection = require("../../living/generated/CDATASection");
const Comment = require("../../living/generated/Comment");
const ProcessingInstruction = require("../../living/generated/ProcessingInstruction");
const Text = require("../../living/generated/Text");
const attributes = require("../../living/attributes");
const { HTML_NS } = require("../../living/helpers/namespaces");
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;
const CUSTOM_NAME_DOCTYPE = /<!doctype\s+([^\s>]+)/i;
function parseDocType(globalObject, ownerDocument, html) {
if (HTML5_DOCTYPE.test(html)) {
return createDocumentType(globalObject, ownerDocument, "html", "", "");
}
const publicPieces = PUBLIC_DOCTYPE.exec(html);
if (publicPieces) {
return createDocumentType(globalObject, ownerDocument, publicPieces[1], publicPieces[2], publicPieces[3]);
}
const systemPieces = SYSTEM_DOCTYPE.exec(html);
if (systemPieces) {
return createDocumentType(globalObject, ownerDocument, systemPieces[1], "", systemPieces[2]);
}
const namePiece = CUSTOM_NAME_DOCTYPE.exec(html)[1] || "html";
return createDocumentType(globalObject, ownerDocument, namePiece, "", "");
}
function createDocumentType(globalObject, ownerDocument, name, publicId, systemId) {
return DocumentType.createImpl(globalObject, [], { ownerDocument, name, publicId, systemId });
}
function isHTMLTemplateElement(element) {
return element.tagName === "template" && element.namespaceURI === HTML_NS;
}
function createParser(rootNode, globalObject, saxesOptions) {
const parser = new SaxesParser({
...saxesOptions,
// Browsers always have namespace support.
xmlns: true,
// We force the parser to treat all documents (even documents declaring themselves to be XML 1.1 documents) as XML
// 1.0 documents. See https://github.com/jsdom/jsdom/issues/2677 for a discussion of the stakes.
defaultXMLVersion: "1.0",
forceXMLVersion: true
});
const openStack = [rootNode];
function getOwnerDocument() {
const currentElement = openStack[openStack.length - 1];
return isHTMLTemplateElement(currentElement) ?
currentElement._templateContents._ownerDocument :
currentElement._ownerDocument;
}
function appendChild(child) {
const parentElement = openStack[openStack.length - 1];
if (isHTMLTemplateElement(parentElement)) {
parentElement._templateContents._insert(child, null);
} else {
parentElement._insert(child, null);
}
}
parser.on("text", saxesOptions.fragment ?
// In a fragment, all text events produced by saxes must result in a text
// node.
data => {
const ownerDocument = getOwnerDocument();
appendChild(Text.createImpl(globalObject, [], { data, ownerDocument }));
} :
// When parsing a whole document, we must ignore those text nodes that are
// produced outside the root element. Saxes produces events for them,
// but DOM trees do not record text outside the root element.
data => {
if (openStack.length > 1) {
const ownerDocument = getOwnerDocument();
appendChild(Text.createImpl(globalObject, [], { data, ownerDocument }));
}
});
parser.on("cdata", data => {
const ownerDocument = getOwnerDocument();
appendChild(CDATASection.createImpl(globalObject, [], { data, ownerDocument }));
});
parser.on("opentag", tag => {
const { local: tagLocal, attributes: tagAttributes } = tag;
const ownerDocument = getOwnerDocument();
const tagNamespace = tag.uri === "" ? null : tag.uri;
const tagPrefix = tag.prefix === "" ? null : tag.prefix;
const isValue = tagAttributes.is === undefined ? null : tagAttributes.is.value;
const elem = createElement(ownerDocument, tagLocal, tagNamespace, tagPrefix, isValue, true);
// We mark a script element as "parser-inserted", which prevents it from
// being immediately executed.
if (tagLocal === "script" && tagNamespace === HTML_NS) {
elem._parserInserted = true;
}
for (const key of Object.keys(tagAttributes)) {
const { prefix, local, uri, value } = tagAttributes[key];
attributes.setAttributeValue(elem, local, value, prefix === "" ? null : prefix, uri === "" ? null : uri);
}
appendChild(elem);
openStack.push(elem);
});
parser.on("closetag", () => {
const elem = openStack.pop();
// Once a script is populated, we can execute it.
if (elem.localName === "script" && elem.namespaceURI === HTML_NS) {
elem._eval();
}
});
parser.on("comment", data => {
const ownerDocument = getOwnerDocument();
appendChild(Comment.createImpl(globalObject, [], { data, ownerDocument }));
});
parser.on("processinginstruction", ({ target, body }) => {
const ownerDocument = getOwnerDocument();
appendChild(ProcessingInstruction.createImpl(globalObject, [], { target, data: body, ownerDocument }));
});
parser.on("doctype", dt => {
const ownerDocument = getOwnerDocument();
appendChild(parseDocType(globalObject, ownerDocument, `<!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.on("error", err => {
throw DOMException.create(globalObject, [err.message, "SyntaxError"]);
});
return parser;
}
function parseFragment(markup, contextElement) {
const { _globalObject, _ownerDocument } = contextElement;
const fragment = DocumentFragment.createImpl(_globalObject, [], { ownerDocument: _ownerDocument });
// Only parseFragment needs resolvePrefix per the saxes documentation:
// https://github.com/lddubeau/saxes#parsing-xml-fragments
const parser = createParser(fragment, _globalObject, {
fragment: true,
resolvePrefix(prefix) {
// saxes wants undefined as the return value if the prefix is not defined, not null.
return contextElement.lookupNamespaceURI(prefix) || undefined;
}
});
parser.write(markup).close();
return fragment;
}
function parseIntoDocument(markup, ownerDocument) {
const { _globalObject } = ownerDocument;
const parser = createParser(ownerDocument, _globalObject, {
fileName: ownerDocument.location && ownerDocument.location.href
});
parser.write(markup).close();
return ownerDocument;
}
module.exports = {
parseFragment,
parseIntoDocument
};
;