"use strict"; const parse5 = require("parse5"); 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("../../living/domparsing/parse5-adapter-serialization"); const OpenElementStack = require("parse5/lib/parser/open-element-stack"); const OpenElementStackOriginalPop = OpenElementStack.prototype.pop; const OpenElementStackOriginalPush = OpenElementStack.prototype.push; class JSDOMParse5Adapter { constructor(documentImpl) { this._documentImpl = documentImpl; // Since the createElement hook doesn't provide the parent element, we keep track of this using _currentElement: // https://github.com/inikulin/parse5/issues/285 this._currentElement = undefined; // Horrible monkey-patch to implement https://github.com/inikulin/parse5/issues/237 const adapter = this; OpenElementStack.prototype.push = function (...args) { OpenElementStackOriginalPush.apply(this, args); adapter._currentElement = this.current; const after = this.items[this.stackTop]; if (after._pushedOnStackOfOpenElements) { after._pushedOnStackOfOpenElements(); } }; OpenElementStack.prototype.pop = function (...args) { const before = this.items[this.stackTop]; OpenElementStackOriginalPop.apply(this, args); adapter._currentElement = this.current; if (before._poppedOffStackOfOpenElements) { before._poppedOffStackOfOpenElements(); } }; } _ownerDocument() { // The _currentElement is undefined when parsing elements at the root of the document. In this case we would // fallback to the global _documentImpl. return this._currentElement ? this._currentElement._ownerDocument : this._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._currentElement._ownerDocument }); } createElement(localName, namespace, attrs) { const ownerDocument = this._ownerDocument(); const element = ownerDocument._createElementWithCorrectElementInterface(localName, namespace); element._namespaceURI = namespace; this.adoptAttributes(element, attrs); if ("_parserInserted" in element) { element._parserInserted = true; } return element; } createCommentNode(data) { const ownerDocument = this._ownerDocument(); return Comment.createImpl([], { data, ownerDocument }); } appendChild(parentNode, newNode) { parentNode._append(newNode); } insertBefore(parentNode, newNode, referenceNode) { parentNode._insert(newNode, referenceNode); } setTemplateContent(templateElement, contentFragment) { // This code makes the glue between jsdom and parse5 HTMLTemplateElement parsing: // // * jsdom during the construction of the HTMLTemplateElement (for example when create via // `document.createElement("template")`), creates a DocumentFragment and set it into _templateContents. // * parse5 when parsing a <template> tag creates an HTMLTemplateElement (`createElement` adapter hook) and also // create a DocumentFragment (`createDocumentFragment` adapter hook). // // At this point we now have to replace the one created in jsdom with one created by parse5. const { _ownerDocument, _host } = templateElement._templateContents; contentFragment._ownerDocument = _ownerDocument; contentFragment._host = _host; templateElement._templateContents = contentFragment; } setDocumentType(document, name, publicId, systemId) { const ownerDocument = this._ownerDocument(); const documentType = DocumentType.createImpl([], { name, publicId, systemId, ownerDocument }); document._append(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 ownerDocument = this._ownerDocument(); const textNode = Text.createImpl([], { data: text, ownerDocument }); parentNode._append(textNode); } } insertTextBefore(parentNode, text, referenceNode) { const { previousSibling } = referenceNode; if (previousSibling && previousSibling.nodeType === nodeTypes.TEXT_NODE) { previousSibling.data += text; } else { const ownerDocument = this._ownerDocument(); const textNode = Text.createImpl([], { data: text, ownerDocument }); parentNode._append(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); } } } // Assign shared adapters with serializer. Object.assign(JSDOMParse5Adapter.prototype, serializationAdapter); function parseFragment(markup, contextElement) { const ownerDocument = contextElement._ownerDocument; const config = Object.assign({}, ownerDocument._parseOptions, { treeAdapter: new JSDOMParse5Adapter(ownerDocument) }); return parse5.parseFragment(contextElement, markup, config); } function parseIntoDocument(markup, ownerDocument) { const config = Object.assign({}, ownerDocument._parseOptions, { treeAdapter: new JSDOMParse5Adapter(ownerDocument) }); return parse5.parse(markup, config); } module.exports = { parseFragment, parseIntoDocument };