Skip to content

Commit

Permalink
Custom element implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
pmdartus committed Jan 15, 2020
1 parent d240291 commit e5d0414
Show file tree
Hide file tree
Showing 26 changed files with 1,398 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
11 changes: 7 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);
};

// https://html.spec.whatwg.org/#the-window-object
function setupWindow(windowInstance, { runScripts }) {
Expand Down Expand Up @@ -576,6 +577,8 @@ function Window(options) {
return window._document.getSelection();
};

this.customElements = CustomElementRegistry.create(window);

// The captureEvents() and releaseEvents() methods must do nothing
this.captureEvents = function () {};

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,15 +13,19 @@ const attributes = require("../../living/attributes");
const nodeTypes = require("../../living/node-type");

const serializationAdapter = require("../../living/domparsing/parse5-adapter-serialization");
const {
CUSTOM_ELEMENT_REACTIONS_STACK, invokeCEReactions, lookupCEDefinition
} = require("../../living/helpers/custom-elements");

const OpenElementStack = require("parse5/lib/parser/open-element-stack");
const OpenElementStackOriginalPop = OpenElementStack.prototype.pop;
const OpenElementStackOriginalPush = OpenElementStack.prototype.push;

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
Expand Down Expand Up @@ -49,9 +55,16 @@ class JSDOMParse5Adapter {
}

_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 @@ -63,16 +76,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._throwOwnDynamicMarkupInsertionCounter++;
CUSTOM_ELEMENT_REACTIONS_STACK.push([]);
}

const element = createElement(ownerDocument, localName, namespace, null, isValue, willExecuteScript);
this.adoptAttributes(element, attrs);

if (willExecuteScript) {
const queue = CUSTOM_ELEMENT_REACTIONS_STACK.pop();
invokeCEReactions(queue);
ownerDocument._throwOwnDynamicMarkupInsertionCounter--;
}

if ("_parserInserted" in element) {
element._parserInserted = true;
}
Expand Down Expand Up @@ -159,10 +194,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
51 changes: 46 additions & 5 deletions lib/jsdom/living/attributes.js
@@ -1,9 +1,11 @@
"use strict";
const DOMException = require("domexception/webidl2js-wrapper");
const attrGenerated = require("./generated/Attr");
const { asciiLowercase } = require("./helpers/strings");

const { HTML_NS } = require("./helpers/namespaces");
const { asciiLowercase } = require("./helpers/strings");
const { queueAttributeMutationRecord } = require("./helpers/mutation-observers");
const { CUSTOM_ELEMENT_STATE, 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 @@ -28,21 +30,40 @@ exports.changeAttribute = function (element, attribute, value) {
// https://dom.spec.whatwg.org/#concept-element-attributes-change

const { _localName, _namespace, _value } = attribute;

queueAttributeMutationRecord(element, _localName, _namespace, _value);

const oldValue = attribute._value;
if (element._ceState === CUSTOM_ELEMENT_STATE.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);
};

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

const { _localName, _namespace } = attribute;
const { _localName, _namespace, _value } = attribute;

queueAttributeMutationRecord(element, _localName, _namespace, null);

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

const attributeList = element._attributeList;

attributeList.push(attribute);
Expand All @@ -59,15 +80,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_ELEMENT_STATE.CUSTOM) {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
_value,
null,
_namespace
]);
}

const attributeList = element._attributeList;

for (let i = 0; i < attributeList.length; ++i) {
Expand Down Expand Up @@ -96,8 +127,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_ELEMENT_STATE.CUSTOM) {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
_value,
newAttr._value,
_namespace
]);
}

const attributeList = element._attributeList;

for (let i = 0; i < attributeList.length; ++i) {
Expand Down
6 changes: 3 additions & 3 deletions lib/jsdom/living/attributes/Attr.webidl
Expand Up @@ -7,9 +7,9 @@ interface Attr {
readonly attribute DOMString localName;
readonly attribute DOMString name;
readonly attribute DOMString nodeName; // historical alias of .name
attribute DOMString value;
[TreatNullAs=EmptyString] attribute DOMString nodeValue; // historical alias of .value
[TreatNullAs=EmptyString] attribute DOMString textContent; // historical alias of .value
[CEReactions] attribute DOMString value;
[CEReactions, TreatNullAs=EmptyString] attribute DOMString nodeValue; // historical alias of .value
[CEReactions, TreatNullAs=EmptyString] attribute DOMString textContent; // historical alias of .value

readonly attribute Element? ownerElement;

Expand Down

0 comments on commit e5d0414

Please sign in to comment.