Skip to content

Commit

Permalink
Add custom element support
Browse files Browse the repository at this point in the history
Fixes #1030.
  • Loading branch information
pmdartus committed Feb 16, 2020
1 parent c3227ce commit 09301da
Show file tree
Hide file tree
Showing 26 changed files with 1,420 additions and 687 deletions.
8 changes: 4 additions & 4 deletions lib/api.js
Expand Up @@ -10,7 +10,7 @@ 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 Window = require("./jsdom/browser/Window.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");
Expand All @@ -33,7 +33,7 @@ class JSDOM {

options = transformOptions(options, encoding, mimeType);

this[window] = new Window(options.windowOptions);
this[window] = createWindow(options.windowOptions);

const documentImpl = idlUtils.implForWrapper(this[window]._document);

Expand All @@ -45,8 +45,8 @@ class JSDOM {
}

get window() {
// It's important to grab the global proxy, instead of just the result of `new Window(...)`, since otherwise things
// like `window.eval` don't exist.
// 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;
}

Expand Down
13 changes: 9 additions & 4 deletions lib/jsdom/browser/Window.js
Expand Up @@ -4,6 +4,7 @@ const webIDLConversions = require("webidl-conversions");
const { CSSStyleDeclaration } = require("cssstyle");
const { Performance: RawPerformance } = require("w3c-hr-time");
const notImplemented = require("./not-implemented");
const { installInterfaces } = require("../living/interfaces");
const { define, mixin } = require("../utils");
const Element = require("../living/generated/Element");
const EventTarget = require("../living/generated/EventTarget");
Expand All @@ -27,15 +28,15 @@ const { fireAnEvent } = require("../living/helpers/events");
const SessionHistory = require("../living/window/SessionHistory");
const { forEachMatchingSheetRuleOfElement, getResolvedValue, propertiesWithResolvedValueImplemented } =
require("../living/helpers/style-rules");
const CustomElementRegistry = require("../living/generated/CustomElementRegistry");
const jsGlobals = require("./js-globals.json");

const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation;
const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation;

// NB: the require() must be after assigning `module.exports` because this require() is circular
// TODO: this above note might not even be true anymore... figure out the cycle and document it, or clean up.
module.exports = Window;
const { installInterfaces } = require("../living/interfaces");
exports.createWindow = function (options) {
return new Window(options);
};

const jsGlobalEntriesToInstall = Object.entries(jsGlobals).filter(([name]) => name in global);

Expand Down Expand Up @@ -223,6 +224,7 @@ function Window(options) {
const navigator = Navigator.create(window, [], { userAgent: this._resourceLoader._userAgent });
const performance = Performance.create(window, [], { rawPerformance });
const screen = Screen.create(window);
const customElementRegistry = CustomElementRegistry.create(window);

define(this, {
get length() {
Expand Down Expand Up @@ -316,6 +318,9 @@ function Window(options) {
}

return this._sessionStorage;
},
get customElements() {
return customElementRegistry;
}
});

Expand Down
57 changes: 48 additions & 9 deletions lib/jsdom/browser/parser/html.js
Expand Up @@ -2,6 +2,8 @@

const parse5 = require("parse5");

const { createElement } = require("../../living/helpers/create-element");

const DocumentType = require("../../living/generated/DocumentType");
const DocumentFragment = require("../../living/generated/DocumentFragment");
const Text = require("../../living/generated/Text");
Expand All @@ -11,6 +13,9 @@ 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.
Expand Down Expand Up @@ -40,19 +45,27 @@ OpenElementStack.prototype.pop = function (...args) {
};

class JSDOMParse5Adapter {
constructor(documentImpl) {
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() {
// 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;
const { _currentElement } = this;

// The _currentElement is undefined when parsing elements at the root of the document.
if (_currentElement) {
return _currentElement.localName === "template" ?
_currentElement.content._ownerDocument :
_currentElement._ownerDocument;
}

return this._documentImpl;
}

createDocument() {
Expand All @@ -64,16 +77,38 @@ class JSDOMParse5Adapter {
}

createDocumentFragment() {
return DocumentFragment.createImpl(this._globalObject, [], { ownerDocument: this._currentElement._ownerDocument });
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 element = ownerDocument._createElementWithCorrectElementInterface(localName, namespace);
element._namespaceURI = namespace;
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;
}
Expand Down Expand Up @@ -160,10 +195,14 @@ class JSDOMParse5Adapter {
Object.assign(JSDOMParse5Adapter.prototype, serializationAdapter);

function parseFragment(markup, contextElement) {
const ownerDocument = contextElement._ownerDocument;
const ownerDocument = contextElement.localName === "template" ?
contextElement.content._ownerDocument :
contextElement._ownerDocument;

const config = Object.assign({}, ownerDocument._parseOptions, {
treeAdapter: new JSDOMParse5Adapter(ownerDocument)
treeAdapter: new JSDOMParse5Adapter(ownerDocument, {
fragment: true
})
});

return parse5.parseFragment(contextElement, markup, config);
Expand Down
15 changes: 9 additions & 6 deletions lib/jsdom/browser/parser/xml.js
Expand Up @@ -3,6 +3,8 @@
const { SaxesParser } = require("saxes");
const DOMException = require("domexception/webidl2js-wrapper");

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");
Expand Down Expand Up @@ -99,17 +101,18 @@ function createParser(rootNode, globalObject, saxesOptions) {
};

parser.onopentag = tag => {
const { local: tagLocal, uri: tagURI, prefix: tagPrefix, attributes: tagAttributes } = tag;
const ownerDocument = getOwnerDocument();
const { local: tagLocal, attributes: tagAttributes } = tag;

const elem = ownerDocument._createElementWithCorrectElementInterface(tagLocal, tagURI);
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;

elem._prefix = tagPrefix || null;
elem._namespaceURI = tagURI || null;
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" && tagURI === HTML_NS) {
if (tagLocal === "script" && tagNamespace === HTML_NS) {
elem._parserInserted = true;
}

Expand Down
50 changes: 45 additions & 5 deletions lib/jsdom/living/attributes.js
@@ -1,8 +1,10 @@
"use strict";
const DOMException = require("domexception/webidl2js-wrapper");
const { asciiLowercase } = require("./helpers/strings");

const { HTML_NS } = require("./helpers/namespaces");
const { asciiLowercase } = require("./helpers/strings");
const { queueAttributeMutationRecord } = require("./helpers/mutation-observers");
const { enqueueCECallbackReaction } = require("./helpers/custom-elements");

// The following three are for https://dom.spec.whatwg.org/#concept-element-attribute-has. We don't just have a
// predicate tester since removing that kind of flexibility gives us the potential for better future optimizations.
Expand All @@ -26,20 +28,38 @@ exports.hasAttributeByNameNS = function (element, namespace, localName) {
// https://dom.spec.whatwg.org/#concept-element-attributes-change
exports.changeAttribute = (element, attribute, value) => {
const { _localName, _namespace, _value } = attribute;

queueAttributeMutationRecord(element, _localName, _namespace, _value);

const oldValue = attribute._value;
if (element._ceState === "custom") {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
_value,
value,
_namespace
]);
}

attribute._value = value;

// Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is changed."
element._attrModified(attribute._qualifiedName, value, oldValue);
element._attrModified(attribute._qualifiedName, value, _value);
};

// https://dom.spec.whatwg.org/#concept-element-attributes-append
exports.appendAttribute = function (element, attribute) {
const { _localName, _namespace } = attribute;
const { _localName, _namespace, _value } = attribute;
queueAttributeMutationRecord(element, _localName, _namespace, null);

if (element._ceState === "custom") {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
null,
_value,
_namespace
]);
}

const attributeList = element._attributeList;

attributeList.push(attribute);
Expand All @@ -56,15 +76,25 @@ exports.appendAttribute = function (element, attribute) {
entry.push(attribute);

// Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is added."
element._attrModified(name, attribute._value, null);
element._attrModified(name, _value, null);
};

exports.removeAttribute = function (element, attribute) {
// https://dom.spec.whatwg.org/#concept-element-attributes-remove

const { _localName, _namespace, _value } = attribute;

queueAttributeMutationRecord(element, _localName, _namespace, _value);

if (element._ceState === "custom") {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
_value,
null,
_namespace
]);
}

const attributeList = element._attributeList;

for (let i = 0; i < attributeList.length; ++i) {
Expand Down Expand Up @@ -93,8 +123,18 @@ exports.replaceAttribute = function (element, oldAttr, newAttr) {
// https://dom.spec.whatwg.org/#concept-element-attributes-replace

const { _localName, _namespace, _value } = oldAttr;

queueAttributeMutationRecord(element, _localName, _namespace, _value);

if (element._ceState === "custom") {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
_value,
newAttr._value,
_namespace
]);
}

const attributeList = element._attributeList;

for (let i = 0; i < attributeList.length; ++i) {
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/attributes/Attr.webidl
Expand Up @@ -6,7 +6,7 @@ interface Attr : Node {
readonly attribute DOMString? prefix;
readonly attribute DOMString localName;
readonly attribute DOMString name;
attribute DOMString value;
[CEReactions] attribute DOMString value;

readonly attribute Element? ownerElement;

Expand Down

0 comments on commit 09301da

Please sign in to comment.