Skip to content

Commit

Permalink
defineElement should upgrade existing unresolved custom elements
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=155107

Reviewed by Darin Adler.

Source/WebCore:

Added the support for upgrading existing unresolved custom elements when defineElement is called.

The current implementation upgrades elements in the order they were created and has the issue that
it keeps accumulating all elements with a hyphen in its name until defineElement is called as
documented in WICG/webcomponents#419

This patch re-purposes IsEditingTextFlag to indicate that the node is an unresolved custom element.
Since isEditingText() is only called in textRendererIsNeeded only on Text nodes, it's mutually
exclusive with isUnresolvedCustomElement().

The list of unresolved custom elements is kept in m_upgradeCandidatesMap, a hash map of element names
to the list of unresolved elements with that name.

In addition, added the logic to use HTMLElement as the interface for unresolved custom element instead
of HTMLUnknownElement.

Test: fast/custom-elements/upgrading/upgrading-parser-created-element.html

* bindings/js/JSCustomElementInterface.cpp:
(WebCore::JSCustomElementInterface::upgradeElement): Clear the flag.
* bindings/js/JSDocumentCustom.cpp:
(WebCore::JSDocument::defineElement): Set the unique private name to keep the interface alive before
calling addElementDefinition as the call can now invoke author scripts.
* dom/CustomElementDefinitions.cpp:
(WebCore::CustomElementDefinitions::addElementDefinition): Upgrade existing unresolved elements kept
in m_upgradeCandidatesMap.
(WebCore::CustomElementDefinitions::addUpgradeCandidate): Added.
* dom/CustomElementDefinitions.h:
* dom/Document.cpp:
(WebCore::createHTMLElementWithNameValidation): Added the code to add the unresolved custom elements
to the upgrade candidates map. Also instantiate it as HTMLElement instead of HTMLUnknownElement.
(WebCore::createFallbackHTMLElement): Ditto.
* dom/Node.h:
(WebCore::Node::setIsCustomElement):
(WebCore::Node::isUnresolvedCustomElement): Added.
(WebCore::Node::setIsUnresolvedCustomElement): Added.
(WebCore::Node::setCustomElementIsResolved): Added. Clears IsEditingTextOrUnresolvedCustomElementFlag
and sets IsCustomElement.
(WebCore::Node::isEditingText): Check both IsEditingTextOrUnresolvedCustomElementFlag and IsTextFlag
for safety even though it's currently only used in textRendererIsNeeded which takes Text&.
* dom/make_names.pl:
(defaultParametersHash): Added customElementInterfaceName as a parameter.
(printWrapperFactoryCppFile): Generate the code to use customElementInterfaceName when the element
for which the wrapper is created has isUnresolvedCustomElement flag set.
* html/HTMLTagNames.in: Use HTMLElement for unresolved custom elements.
* html/parser/HTMLConstructionSite.cpp:
(WebCore::HTMLConstructionSite::createHTMLElementOrFindCustomElementInterface): Added the code to add
the unresolved custom elements to the upgrade candidates map. Also instantiate it as HTMLElement instead
of HTMLUnknownElement. 

LayoutTests:

Added W3C style testharness.js tests for asynchronously defining custom elements.

* fast/custom-elements/upgrading/Node-cloneNode.html:
* fast/custom-elements/upgrading/upgrading-parser-created-element-expected.txt: Added.
* fast/custom-elements/upgrading/upgrading-parser-created-element.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@197917 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
rniwa@webkit.org committed Mar 10, 2016
1 parent 67fa249 commit 7df594e
Show file tree
Hide file tree
Showing 14 changed files with 274 additions and 18 deletions.
13 changes: 13 additions & 0 deletions LayoutTests/ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
2016-03-09 Ryosuke Niwa <rniwa@webkit.org>

defineElement should upgrade existing unresolved custom elements
https://bugs.webkit.org/show_bug.cgi?id=155107

Reviewed by Darin Adler.

Added W3C style testharness.js tests for asynchronously defining custom elements.

* fast/custom-elements/upgrading/Node-cloneNode.html:
* fast/custom-elements/upgrading/upgrading-parser-created-element-expected.txt: Added.
* fast/custom-elements/upgrading/upgrading-parser-created-element.html: Added.

2016-03-09 Saam Barati <sbarati@apple.com>

ES6: Implement lexical scoping for function definitions in strict mode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Custom Elements: Extensions to Document interface</title>
<title>Custom Elements: Upgrading</title>
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
<meta name="assert" content="Node.prototype.cloneNode should upgrade a custom element">
<link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#upgrading">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

PASS Element.prototype.createElement must add an unresolved custom element to the upgrade candidates map
PASS HTMLElement constructor must throw an InvalidStateError when the top of the construction stack is marked AlreadyConstructed due to a custom element constructor constructing itself after super() call
PASS HTMLElement constructor must throw an InvalidStateError when the top of the construction stack is marked AlreadyConstructed due to a custom element constructor constructing itself before super() call
PASS Upgrading a custom element must throw an InvalidStateError when the returned element is not SameValue as the upgraded element

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<title>Custom Elements: Upgrading unresolved elements</title>
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
<meta name="assert" content="HTML parser must add an unresolved custom element to the upgrade candidates map">
<link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#upgrading">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<link rel='stylesheet' href='../../../resources/testharness.css'>
</head>
<body>
<div id="log"></div>
<my-custom-element></my-custom-element>
<instantiates-itself-after-super></instantiates-itself-after-super>
<instantiates-itself-before-super></instantiates-itself-before-super>
<my-other-element id="instance"></my-other-element>
<my-other-element id="otherInstance"></my-other-element>
<script>

test(function () {
class MyCustomElement extends HTMLElement { }

var instance = document.querySelector('my-custom-element');
assert_true(instance instanceof HTMLElement);
assert_false(instance instanceof HTMLUnknownElement,
'an unresolved custom element should not be an instance of HTMLUnknownElement');
assert_false(instance instanceof MyCustomElement);

document.defineElement('my-custom-element', MyCustomElement);

assert_true(instance instanceof HTMLElement);
assert_true(instance instanceof MyCustomElement,
'Calling defineElement must upgrade existing custom elements');

}, 'Element.prototype.createElement must add an unresolved custom element to the upgrade candidates map');


test(function () {
class InstantiatesItselfAfterSuper extends HTMLElement {
constructor(doNotCreateItself) {
super();
if (!doNotCreateItself)
new InstantiatesItselfAfterSuper(true);
}
}

assert_throws({'name': 'InvalidStateError'}, function () {
document.defineElement('instantiates-itself-after-super', InstantiatesItselfAfterSuper);
});
}, 'HTMLElement constructor must throw an InvalidStateError when the top of the construction stack is marked AlreadyConstructed'
+ ' due to a custom element constructor constructing itself after super() call');

test(function () {
class InstantiatesItselfBeforeSuper extends HTMLElement {
constructor(doNotCreateItself) {
if (!doNotCreateItself)
new InstantiatesItselfBeforeSuper(true);
super();
}
}

assert_throws({'name': 'InvalidStateError'}, function () {
document.defineElement('instantiates-itself-before-super', InstantiatesItselfBeforeSuper);
});
}, 'HTMLElement constructor must throw an InvalidStateError when the top of the construction stack is marked AlreadyConstructed'
+ ' due to a custom element constructor constructing itself before super() call');

test(function () {
class MyOtherElement extends HTMLElement {
constructor() {
super();
if (this == instance)
return otherInstance;
}
}
var instance = document.getElementById('instance');
var otherInstance = document.getElementById('otherInstance');

assert_false(instance instanceof MyOtherElement);
assert_false(otherInstance instanceof MyOtherElement);

assert_throws({'name': 'InvalidStateError'}, function () {
document.defineElement('my-other-element', MyOtherElement);
});

assert_true(document.createElement('my-other-element') instanceof MyOtherElement,
'Upgrading of custom elements must happen after the definition was added to the registry.');
}, 'Upgrading a custom element must throw an InvalidStateError when the returned element is not SameValue as the upgraded element');

</script>
</body>
</html>
57 changes: 57 additions & 0 deletions Source/WebCore/ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,60 @@
2016-03-09 Ryosuke Niwa <rniwa@webkit.org>

defineElement should upgrade existing unresolved custom elements
https://bugs.webkit.org/show_bug.cgi?id=155107

Reviewed by Darin Adler.

Added the support for upgrading existing unresolved custom elements when defineElement is called.

The current implementation upgrades elements in the order they were created and has the issue that
it keeps accumulating all elements with a hyphen in its name until defineElement is called as
documented in https://github.com/w3c/webcomponents/issues/419

This patch re-purposes IsEditingTextFlag to indicate that the node is an unresolved custom element.
Since isEditingText() is only called in textRendererIsNeeded only on Text nodes, it's mutually
exclusive with isUnresolvedCustomElement().

The list of unresolved custom elements is kept in m_upgradeCandidatesMap, a hash map of element names
to the list of unresolved elements with that name.

In addition, added the logic to use HTMLElement as the interface for unresolved custom element instead
of HTMLUnknownElement.

Test: fast/custom-elements/upgrading/upgrading-parser-created-element.html

* bindings/js/JSCustomElementInterface.cpp:
(WebCore::JSCustomElementInterface::upgradeElement): Clear the flag.
* bindings/js/JSDocumentCustom.cpp:
(WebCore::JSDocument::defineElement): Set the unique private name to keep the interface alive before
calling addElementDefinition as the call can now invoke author scripts.
* dom/CustomElementDefinitions.cpp:
(WebCore::CustomElementDefinitions::addElementDefinition): Upgrade existing unresolved elements kept
in m_upgradeCandidatesMap.
(WebCore::CustomElementDefinitions::addUpgradeCandidate): Added.
* dom/CustomElementDefinitions.h:
* dom/Document.cpp:
(WebCore::createHTMLElementWithNameValidation): Added the code to add the unresolved custom elements
to the upgrade candidates map. Also instantiate it as HTMLElement instead of HTMLUnknownElement.
(WebCore::createFallbackHTMLElement): Ditto.
* dom/Node.h:
(WebCore::Node::setIsCustomElement):
(WebCore::Node::isUnresolvedCustomElement): Added.
(WebCore::Node::setIsUnresolvedCustomElement): Added.
(WebCore::Node::setCustomElementIsResolved): Added. Clears IsEditingTextOrUnresolvedCustomElementFlag
and sets IsCustomElement.
(WebCore::Node::isEditingText): Check both IsEditingTextOrUnresolvedCustomElementFlag and IsTextFlag
for safety even though it's currently only used in textRendererIsNeeded which takes Text&.
* dom/make_names.pl:
(defaultParametersHash): Added customElementInterfaceName as a parameter.
(printWrapperFactoryCppFile): Generate the code to use customElementInterfaceName when the element
for which the wrapper is created has isUnresolvedCustomElement flag set.
* html/HTMLTagNames.in: Use HTMLElement for unresolved custom elements.
* html/parser/HTMLConstructionSite.cpp:
(WebCore::HTMLConstructionSite::createHTMLElementOrFindCustomElementInterface): Added the code to add
the unresolved custom elements to the upgrade candidates map. Also instantiate it as HTMLElement instead
of HTMLUnknownElement.

2016-03-09 Enrica Casucci <enrica@apple.com>

Retrieve additional context for some data detector link for preview and action menu.
Expand Down
3 changes: 2 additions & 1 deletion Source/WebCore/bindings/js/JSCustomElementInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ RefPtr<Element> JSCustomElementInterface::constructElement(const AtomicString& t

void JSCustomElementInterface::upgradeElement(Element& element)
{
ASSERT(element.isCustomElement());
ASSERT(element.isUnresolvedCustomElement());
if (!canInvokeCallback())
return;

Expand Down Expand Up @@ -147,6 +147,7 @@ void JSCustomElementInterface::upgradeElement(Element& element)
throwInvalidStateError(*state, "Custom element constructor failed to upgrade an element");
return;
}
wrappedElement->setCustomElementIsResolved();
ASSERT(wrappedElement->isCustomElement());
}

Expand Down
5 changes: 3 additions & 2 deletions Source/WebCore/bindings/js/JSDocumentCustom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,12 @@ JSValue JSDocument::defineElement(ExecState& state)
// FIXME: 13. Let detachedCallback be Get(prototype, "detachedCallback"). Rethrow any exceptions.
// FIXME: 14. Let attributeChangedCallback be Get(prototype, "attributeChangedCallback"). Rethrow any exceptions.

QualifiedName name(nullAtom, tagName, HTMLNames::xhtmlNamespaceURI);
definitions.addElementDefinition(JSCustomElementInterface::create(name, object, globalObject()));
PrivateName uniquePrivateName;
globalObject()->putDirect(globalObject()->vm(), uniquePrivateName, object);

QualifiedName name(nullAtom, tagName, HTMLNames::xhtmlNamespaceURI);
definitions.addElementDefinition(JSCustomElementInterface::create(name, object, globalObject()));

// FIXME: 17. Let map be registry's upgrade candidates map.
// FIXME: 18. Upgrade a newly-defined element given map and definition.

Expand Down
28 changes: 27 additions & 1 deletion Source/WebCore/dom/CustomElementDefinitions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,33 @@ void CustomElementDefinitions::addElementDefinition(Ref<JSCustomElementInterface
AtomicString localName = interface->name().localName();
ASSERT(!m_nameMap.contains(localName));
m_constructorMap.add(interface->constructor(), interface.ptr());
m_nameMap.add(localName, WTFMove(interface));
m_nameMap.add(localName, interface.copyRef());

auto candidateList = m_upgradeCandidatesMap.find(localName);
if (candidateList == m_upgradeCandidatesMap.end())
return;

Vector<RefPtr<Element>> list(WTFMove(candidateList->value));

m_upgradeCandidatesMap.remove(localName);

for (auto& candidate : list) {
ASSERT(candidate);
interface->upgradeElement(*candidate);
}

// We should not be adding more upgrade candidate for this local name.
ASSERT(!m_upgradeCandidatesMap.contains(localName));
}

void CustomElementDefinitions::addUpgradeCandidate(Element& candidate)
{
auto result = m_upgradeCandidatesMap.ensure(candidate.localName(), [] {
return Vector<RefPtr<Element>>();
});
auto& nodeVector = result.iterator->value;
ASSERT(!nodeVector.contains(&candidate));
nodeVector.append(&candidate);
}

JSCustomElementInterface* CustomElementDefinitions::findInterface(const QualifiedName& name) const
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/dom/CustomElementDefinitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class CustomElementDefinitions {
WTF_MAKE_FAST_ALLOCATED;
public:
void addElementDefinition(Ref<JSCustomElementInterface>&&);
void addUpgradeCandidate(Element&);

JSCustomElementInterface* findInterface(const QualifiedName&) const;
JSCustomElementInterface* findInterface(const AtomicString&) const;
Expand All @@ -59,6 +60,7 @@ class CustomElementDefinitions {
static NameStatus checkName(const AtomicString& tagName);

private:
HashMap<AtomicString, Vector<RefPtr<Element>>> m_upgradeCandidatesMap;
HashMap<AtomicString, RefPtr<JSCustomElementInterface>> m_nameMap;
HashMap<const JSC::JSObject*, JSCustomElementInterface*> m_constructorMap;
};
Expand Down
22 changes: 20 additions & 2 deletions Source/WebCore/dom/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,18 @@ static RefPtr<Element> createHTMLElementWithNameValidation(Document& document, c
return nullptr;
}

return HTMLUnknownElement::create(QualifiedName(nullAtom, localName, xhtmlNamespaceURI), document);
QualifiedName qualifiedName(nullAtom, localName, xhtmlNamespaceURI);

#if ENABLE(CUSTOM_ELEMENTS)
if (CustomElementDefinitions::checkName(localName) == CustomElementDefinitions::NameStatus::Valid) {
Ref<HTMLElement> element = HTMLElement::create(qualifiedName, document);
element->setIsUnresolvedCustomElement();
document.ensureCustomElementDefinitions().addUpgradeCandidate(element.get());
return WTFMove(element);
}
#endif

return HTMLUnknownElement::create(qualifiedName, document);
}

RefPtr<Element> Document::createElementForBindings(const AtomicString& name, ExceptionCode& ec)
Expand Down Expand Up @@ -1080,11 +1091,18 @@ static Ref<HTMLElement> createFallbackHTMLElement(Document& document, const Qual
if (UNLIKELY(definitions)) {
if (auto* interface = definitions->findInterface(name)) {
Ref<HTMLElement> element = HTMLElement::create(name, document);
element->setIsCustomElement(); // Pre-upgrade element is still considered a custom element.
element->setIsUnresolvedCustomElement();
LifecycleCallbackQueue::enqueueElementUpgrade(element.get(), *interface);
return element;
}
}
// FIXME: Should we also check the equality of prefix between the custom element and name?
if (CustomElementDefinitions::checkName(name.localName()) == CustomElementDefinitions::NameStatus::Valid) {
Ref<HTMLElement> element = HTMLElement::create(name, document);
element->setIsUnresolvedCustomElement();
document.ensureCustomElementDefinitions().addUpgradeCandidate(element.get());
return element;
}
#endif
return HTMLUnknownElement::create(name, document);
}
Expand Down
22 changes: 18 additions & 4 deletions Source/WebCore/dom/Node.h
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,11 @@ class Node : public EventTarget {

#if ENABLE(CUSTOM_ELEMENTS)
bool isCustomElement() const { return getFlag(IsCustomElement); }
void setIsCustomElement() { return setFlag(IsCustomElement); }
void setIsCustomElement() { setFlag(IsCustomElement); }

bool isUnresolvedCustomElement() const { return isElementNode() && getFlag(IsEditingTextOrUnresolvedCustomElementFlag); }
void setIsUnresolvedCustomElement() { setFlag(IsEditingTextOrUnresolvedCustomElementFlag); }
void setCustomElementIsResolved();
#endif

// Returns null, a child of ShadowRoot, or a legacy shadow root.
Expand Down Expand Up @@ -320,7 +324,7 @@ class Node : public EventTarget {
StyleChangeType styleChangeType() const { return static_cast<StyleChangeType>(m_nodeFlags & StyleChangeMask); }
bool childNeedsStyleRecalc() const { return getFlag(ChildNeedsStyleRecalcFlag); }
bool styleIsAffectedByPreviousSibling() const { return getFlag(StyleIsAffectedByPreviousSibling); }
bool isEditingText() const { return getFlag(IsEditingTextFlag); }
bool isEditingText() const { return getFlag(IsTextFlag) && getFlag(IsEditingTextOrUnresolvedCustomElementFlag); }

void setChildNeedsStyleRecalc() { setFlag(ChildNeedsStyleRecalcFlag); }
void clearChildNeedsStyleRecalc() { m_nodeFlags &= ~(ChildNeedsStyleRecalcFlag | DirectChildNeedsStyleRecalcFlag); }
Expand Down Expand Up @@ -595,7 +599,7 @@ class Node : public EventTarget {
IsParsingChildrenFinishedFlag = 1 << 13, // Element

StyleChangeMask = 1 << nodeStyleChangeShift | 1 << (nodeStyleChangeShift + 1) | 1 << (nodeStyleChangeShift + 2),
IsEditingTextFlag = 1 << 17,
IsEditingTextOrUnresolvedCustomElementFlag = 1 << 17,
IsNamedFlowContentNodeFlag = 1 << 18,
HasSyntheticAttrChildNodesFlag = 1 << 19,
HasCustomStyleResolveCallbacksFlag = 1 << 20,
Expand Down Expand Up @@ -634,7 +638,7 @@ class Node : public EventTarget {
CreateHTMLElement = CreateStyledElement | IsHTMLFlag,
CreateSVGElement = CreateStyledElement | IsSVGFlag | HasCustomStyleResolveCallbacksFlag,
CreateDocument = CreateContainer | InDocumentFlag,
CreateEditingText = CreateText | IsEditingTextFlag,
CreateEditingText = CreateText | IsEditingTextOrUnresolvedCustomElementFlag,
CreateMathMLElement = CreateStyledElement | IsMathMLFlag
};
Node(Document&, ConstructionType);
Expand Down Expand Up @@ -769,6 +773,16 @@ inline ContainerNode* Node::parentNodeGuaranteedHostFree() const
return parentNode();
}

#if ENABLE(CUSTOM_ELEMENTS)

inline void Node::setCustomElementIsResolved()
{
clearFlag(IsEditingTextOrUnresolvedCustomElementFlag);
setFlag(IsCustomElement);
}

#endif

} // namespace WebCore

#if ENABLE(TREE_DEBUGGING)
Expand Down
15 changes: 15 additions & 0 deletions Source/WebCore/dom/make_names.pl
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ sub defaultParametersHash
'attrsNullNamespace' => 0,
'fallbackInterfaceName' => '',
'fallbackJSInterfaceName' => '',
'customElementInterfaceName' => '',
);
}

Expand Down Expand Up @@ -1318,6 +1319,20 @@ END
populate$parameters{namespace}WrapperMap(functions);
if (auto function = functions.get().get(element->localName().impl()))
return function(globalObject, element);
END
;

if ($parameters{customElementInterfaceName}) {
print F <<END
#if ENABLE(CUSTOM_ELEMENTS)
if (element->isUnresolvedCustomElement())
return CREATE_DOM_WRAPPER(globalObject, $parameters{customElementInterfaceName}, element.get());
#endif
END
;
}

print F <<END
return CREATE_DOM_WRAPPER(globalObject, $parameters{fallbackJSInterfaceName}, element.get());
}
Expand Down
Loading

0 comments on commit 7df594e

Please sign in to comment.