mirror of
https://github.com/idanoo/GoScrobble
synced 2025-07-02 06:02:19 +00:00
223 lines
7.7 KiB
JavaScript
223 lines
7.7 KiB
JavaScript
"use strict";
|
|
|
|
const parse5 = require("parse5");
|
|
|
|
const { createElement } = require("../../living/helpers/create-element");
|
|
const { HTML_NS } = require("../../living/helpers/namespaces");
|
|
|
|
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 {
|
|
customElementReactionsStack, invokeCEReactions, lookupCEDefinition
|
|
} = require("../../living/helpers/custom-elements");
|
|
|
|
// Horrible monkey-patch to implement https://github.com/inikulin/parse5/issues/237 and
|
|
// https://github.com/inikulin/parse5/issues/285.
|
|
const OpenElementStack = require("parse5/lib/parser/open-element-stack");
|
|
|
|
const openElementStackOriginalPush = OpenElementStack.prototype.push;
|
|
OpenElementStack.prototype.push = function (...args) {
|
|
openElementStackOriginalPush.apply(this, args);
|
|
this.treeAdapter._currentElement = this.current;
|
|
|
|
const after = this.items[this.stackTop];
|
|
if (after._pushedOnStackOfOpenElements) {
|
|
after._pushedOnStackOfOpenElements();
|
|
}
|
|
};
|
|
|
|
const openElementStackOriginalPop = OpenElementStack.prototype.pop;
|
|
OpenElementStack.prototype.pop = function (...args) {
|
|
const before = this.items[this.stackTop];
|
|
|
|
openElementStackOriginalPop.apply(this, args);
|
|
this.treeAdapter._currentElement = this.current;
|
|
|
|
if (before._poppedOffStackOfOpenElements) {
|
|
before._poppedOffStackOfOpenElements();
|
|
}
|
|
};
|
|
|
|
class JSDOMParse5Adapter {
|
|
constructor(documentImpl, options = {}) {
|
|
this._documentImpl = documentImpl;
|
|
this._globalObject = documentImpl._globalObject;
|
|
this._fragment = options.fragment || false;
|
|
|
|
// Since the createElement hook doesn't provide the parent element, we keep track of this using _currentElement:
|
|
// https://github.com/inikulin/parse5/issues/285. See above horrible monkey-patch for how this is maintained.
|
|
this._currentElement = undefined;
|
|
}
|
|
|
|
_ownerDocument() {
|
|
const { _currentElement } = this;
|
|
|
|
// The _currentElement is undefined when parsing elements at the root of the document.
|
|
if (_currentElement) {
|
|
return _currentElement.localName === "template" && _currentElement.namespaceURI === HTML_NS ?
|
|
_currentElement.content._ownerDocument :
|
|
_currentElement._ownerDocument;
|
|
}
|
|
|
|
return 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() {
|
|
const ownerDocument = this._ownerDocument();
|
|
return DocumentFragment.createImpl(this._globalObject, [], { ownerDocument });
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/#create-an-element-for-the-token
|
|
createElement(localName, namespace, attrs) {
|
|
const ownerDocument = this._ownerDocument();
|
|
|
|
const isAttribute = attrs.find(attr => attr.name === "is");
|
|
const isValue = isAttribute ? isAttribute.value : null;
|
|
|
|
const definition = lookupCEDefinition(ownerDocument, namespace, localName);
|
|
|
|
let willExecuteScript = false;
|
|
if (definition !== null && !this._fragment) {
|
|
willExecuteScript = true;
|
|
}
|
|
|
|
if (willExecuteScript) {
|
|
ownerDocument._throwOnDynamicMarkupInsertionCounter++;
|
|
customElementReactionsStack.push([]);
|
|
}
|
|
|
|
const element = createElement(ownerDocument, localName, namespace, null, isValue, willExecuteScript);
|
|
this.adoptAttributes(element, attrs);
|
|
|
|
if (willExecuteScript) {
|
|
const queue = customElementReactionsStack.pop();
|
|
invokeCEReactions(queue);
|
|
ownerDocument._throwOnDynamicMarkupInsertionCounter--;
|
|
}
|
|
|
|
if ("_parserInserted" in element) {
|
|
element._parserInserted = true;
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
createCommentNode(data) {
|
|
const ownerDocument = this._ownerDocument();
|
|
return Comment.createImpl(this._globalObject, [], { 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(this._globalObject, [], { 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(this._globalObject, [], { 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(this._globalObject, [], { 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.localName === "template" && contextElement.namespaceURI === HTML_NS ?
|
|
contextElement.content._ownerDocument :
|
|
contextElement._ownerDocument;
|
|
|
|
const config = {
|
|
...ownerDocument._parseOptions,
|
|
treeAdapter: new JSDOMParse5Adapter(ownerDocument, { fragment: true })
|
|
};
|
|
|
|
return parse5.parseFragment(contextElement, markup, config);
|
|
}
|
|
|
|
function parseIntoDocument(markup, ownerDocument) {
|
|
const config = {
|
|
...ownerDocument._parseOptions,
|
|
treeAdapter: new JSDOMParse5Adapter(ownerDocument)
|
|
};
|
|
|
|
return parse5.parse(markup, config);
|
|
}
|
|
|
|
module.exports = {
|
|
parseFragment,
|
|
parseIntoDocument
|
|
};
|