LibWeb: Implement scoped custom element registries

This commit is contained in:
Sam Atkins
2026-02-27 17:05:47 +00:00
committed by Tim Ledbetter
parent 90c39c214d
commit ed6a5f25a0
Notes: github-actions[bot] 2026-03-27 19:50:58 +00:00
48 changed files with 1243 additions and 749 deletions

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -23,6 +24,15 @@ namespace Web::HTML {
GC_DEFINE_ALLOCATOR(CustomElementRegistry);
GC_DEFINE_ALLOCATOR(CustomElementDefinition);
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry
GC::Ref<CustomElementRegistry> CustomElementRegistry::construct_impl(JS::Realm& realm)
{
// The new CustomElementRegistry() constructor steps are to set this's is scoped to true.
auto registry = realm.create<CustomElementRegistry>(realm);
registry->m_is_scoped = true;
return registry;
}
CustomElementRegistry::CustomElementRegistry(JS::Realm& realm)
: Bindings::PlatformObject(realm)
{
@@ -126,7 +136,8 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
if (!is_valid_custom_element_name(name))
return JS::throw_completion(WebIDL::SyntaxError::create(realm, Utf16String::formatted("'{}' is not a valid custom element name", name)));
// 3. If this's custom element definition set contains an item with name name, then throw a "NotSupportedError" DOMException.
// 3. If this's custom element definition set contains an item with name name, then throw a "NotSupportedError"
// DOMException.
auto existing_definition_with_name_iterator = m_custom_element_definitions.find_if([&name](auto const& definition) {
return definition->name() == name;
});
@@ -134,7 +145,8 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
if (existing_definition_with_name_iterator != m_custom_element_definitions.end())
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, Utf16String::formatted("A custom element with name '{}' is already defined", name)));
// 4. If this's custom element definition set contains an item with constructor constructor, then throw a "NotSupportedError" DOMException.
// 4. If this's custom element definition set contains an item with constructor constructor, then throw a
// "NotSupportedError" DOMException.
auto existing_definition_with_constructor_iterator = m_custom_element_definitions.find_if([&constructor](auto const& definition) {
return definition->constructor().callback == constructor->callback;
});
@@ -150,17 +162,20 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
// 7. If extends is not null:
if (extends.has_value()) {
// 1. If extends is a valid custom element name, then throw a "NotSupportedError" DOMException.
// 1. If this's is scoped is true, then throw a "NotSupportedError" DOMException.
if (m_is_scoped)
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Cannot define a custom element that extends another in a scoped registry"_utf16));
// 2. If extends is a valid custom element name, then throw a "NotSupportedError" DOMException.
if (is_valid_custom_element_name(extends.value()))
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, Utf16String::formatted("'{}' is a custom element name, only non-custom elements can be extended", extends.value())));
// 2. If the element interface for extends and the HTML namespace is HTMLUnknownElement
// (e.g., if extends does not indicate an element definition in this specification),
// then throw a "NotSupportedError" DOMException.
// 3. If the element interface for extends and the HTML namespace is HTMLUnknownElement (e.g., if extends does
// not indicate an element definition in this specification), then throw a "NotSupportedError" DOMException.
if (DOM::is_unknown_html_element(extends.value()))
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, Utf16String::formatted("'{}' is an unknown HTML element", extends.value())));
// 3. Set localName to extends.
// 4. Set localName to extends.
local_name = extends.value();
}
@@ -183,11 +198,8 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
// 13. Let observedAttributes be an empty sequence<DOMString>.
Vector<String> observed_attributes;
// NOTE: This is not in the spec, but is required because of how we catch the exception by using a lambda, meaning we need to define this
// variable outside of it to use it later.
OrderedHashMap<FlyString, GC::Root<WebIDL::CallbackType>> lifecycle_callbacks;
// 14. Run the following steps while catching any exceptions:
OrderedHashMap<FlyString, GC::Root<WebIDL::CallbackType>> lifecycle_callbacks;
auto get_definition_attributes_from_constructor = [&]() -> JS::ThrowCompletionOr<void> {
// 1. Let prototype be ? Get(constructor, "prototype").
auto prototype_value = TRY(constructor->callback->get(vm.names.prototype));
@@ -197,8 +209,8 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
if (!prototype)
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, prototype_value);
// 3. Let lifecycleCallbacks be the ordered map «[ "connectedCallback" → null, "disconnectedCallback" → null, "adoptedCallback" → null,
// "connectedMoveCallback" → null, "attributeChangedCallback" → null ]».
// 3. Let lifecycleCallbacks be the ordered map «[ "connectedCallback" → null, "disconnectedCallback" → null,
// "adoptedCallback" → null, "connectedMoveCallback" → null, "attributeChangedCallback" → null ]».
lifecycle_callbacks.set(CustomElementReactionNames::connectedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::disconnectedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::adoptedCallback, {});
@@ -210,7 +222,7 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
// 1. Let callbackValue be ? Get(prototype, callbackName).
auto callback_value = TRY(prototype->get(Utf16FlyString::from_utf8(callback_name)));
// 2. If callbackValue is not undefined, then set the value of the entry in lifecycleCallbacks with key callbackName to the result of
// 2. If callbackValue is not undefined, then set lifecycleCallbacks[callbackName] to the result of
// converting callbackValue to the Web IDL Function callback type.
if (!callback_value.is_undefined()) {
auto callback = TRY(convert_value_to_callback_function(vm, callback_value));
@@ -225,8 +237,9 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
// 1. Let observedAttributesIterable be ? Get(constructor, "observedAttributes").
auto observed_attributes_iterable = TRY(constructor->callback->get(vm.names.observedAttributes));
// 2. If observedAttributesIterable is not undefined, then set observedAttributes to the result of converting observedAttributesIterable
// to a sequence<DOMString>. Rethrow any exceptions from the conversion.
// 2. If observedAttributesIterable is not undefined, then set observedAttributes to the result of
// converting observedAttributesIterable to a sequence<DOMString>. Rethrow any exceptions from the
// conversion.
if (!observed_attributes_iterable.is_undefined())
observed_attributes = TRY(convert_value_to_sequence_of_strings(vm, observed_attributes_iterable));
}
@@ -237,8 +250,8 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
// 7. Let disabledFeaturesIterable be ? Get(constructor, "disabledFeatures").
auto disabled_features_iterable = TRY(constructor->callback->get(vm.names.disabledFeatures));
// 8. If disabledFeaturesIterable is not undefined, then set disabledFeatures to the result of converting disabledFeaturesIterable to a
// sequence<DOMString>. Rethrow any exceptions from the conversion.
// 8. If disabledFeaturesIterable is not undefined, then set disabledFeatures to the result of converting
// disabledFeaturesIterable to a sequence<DOMString>. Rethrow any exceptions from the conversion.
if (!disabled_features_iterable.is_undefined())
disabled_features = TRY(convert_value_to_sequence_of_strings(vm, disabled_features_iterable));
@@ -254,15 +267,15 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
// 12. Set formAssociated to the result of converting formAssociatedValue to a boolean.
form_associated = form_associated_value.to_boolean();
// 13. If formAssociated is true, then for each callbackName of « "formAssociatedCallback", "formResetCallback", "formDisabledCallback",
// "formStateRestoreCallback" »:
// 13. If formAssociated is true, then for each callbackName of « "formAssociatedCallback",
// "formResetCallback", "formDisabledCallback", "formStateRestoreCallback" »:
if (form_associated) {
for (auto const& callback_name : { CustomElementReactionNames::formAssociatedCallback, CustomElementReactionNames::formResetCallback, CustomElementReactionNames::formDisabledCallback, CustomElementReactionNames::formStateRestoreCallback }) {
// 1. Let callbackValue be ? Get(prototype, callbackName).
auto callback_value = TRY(prototype->get(Utf16FlyString::from_utf8(callback_name)));
// 2. If callbackValue is not undefined, then set lifecycleCallbacks[callbackName] to the result of converting callbackValue
// to the Web IDL Function callback type.
// 2. If callbackValue is not undefined, then set lifecycleCallbacks[callbackName] to the result of
// converting callbackValue to the Web IDL Function callback type.
if (!callback_value.is_undefined())
lifecycle_callbacks.set(callback_name, TRY(convert_value_to_callback_function(vm, callback_value)));
}
@@ -273,46 +286,36 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
auto maybe_exception = get_definition_attributes_from_constructor();
// Then, regardless of whether the above steps threw an exception or not: set this's element definition is running to false.
// Then, regardless of whether the above steps threw an exception or not: set this's element definition is
// running to false.
m_element_definition_is_running = false;
// Finally, if the steps threw an exception, rethrow that exception.
if (maybe_exception.is_throw_completion())
return maybe_exception.release_error();
// 15. Let definition be a new custom element definition with name name, local name localName, constructor constructor,
// observed attributes observedAttributes, lifecycle callbacks lifecycleCallbacks, form-associated formAssociated,
// disable internals disableInternals, and disable shadow disableShadow.
// 15. Let definition be a new custom element definition with name name, local name localName, constructor
// constructor, observed attributes observedAttributes, lifecycle callbacks lifecycleCallbacks, form-associated
// formAssociated, disable internals disableInternals, and disable shadow disableShadow.
auto definition = CustomElementDefinition::create(realm, name, local_name, *constructor, move(observed_attributes), move(lifecycle_callbacks), form_associated, disable_internals, disable_shadow);
// 16. Append definition to this's custom element definition set.
m_custom_element_definitions.append(definition);
// 17. Let document be this's relevant global object's associated Document.
auto& document = as<HTML::Window>(relevant_global_object(*this)).associated_document();
// 17. If this's is scoped is true, then for each document of this's scoped document set:
// upgrade particular elements within a document given this, document, definition, and localName.
if (m_is_scoped) {
for (auto& document : m_scoped_documents)
document.upgrade_particular_elements(*this, definition, local_name);
}
// 18. Otherwise, upgrade particular elements within a document given this, this's relevant global object's
// associated Document, definition, localName, and name.
else {
auto& document = as<HTML::Window>(relevant_global_object(*this)).associated_document();
document.upgrade_particular_elements(*this, definition, local_name, name);
}
// 18. Let upgradeCandidates be all elements that are shadow-including descendants of document, whose namespace is the HTML namespace
// and whose local name is localName, in shadow-including tree order.
// Additionally, if extends is non-null, only include elements whose is value is equal to name.
Vector<GC::Root<DOM::Element>> upgrade_candidates;
document.for_each_shadow_including_descendant([&](DOM::Node& inclusive_descendant) {
if (!is<DOM::Element>(inclusive_descendant))
return TraversalDecision::Continue;
auto& inclusive_descendant_element = static_cast<DOM::Element&>(inclusive_descendant);
if (inclusive_descendant_element.namespace_uri() == Namespace::HTML && inclusive_descendant_element.local_name() == local_name && (!extends.has_value() || inclusive_descendant_element.is_value() == name))
upgrade_candidates.append(GC::make_root(inclusive_descendant_element));
return TraversalDecision::Continue;
});
// 19. For each element element of upgradeCandidates, enqueue a custom element upgrade reaction given element and definition.
for (auto& element : upgrade_candidates)
element->enqueue_a_custom_element_upgrade_reaction(definition);
// 20. If this's when-defined promise map[name] exists:
// 19. If this's when-defined promise map[name] exists:
auto promise_when_defined_iterator = m_when_defined_promise_map.find(name);
if (promise_when_defined_iterator != m_when_defined_promise_map.end()) {
// 1. Resolve this's when-defined promise map[name] with constructor.
@@ -390,22 +393,75 @@ WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> CustomElementRegistry::when_define
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-upgrade
void CustomElementRegistry::upgrade(GC::Ref<DOM::Node> root) const
{
// 1. Let candidates be a list of all of root's shadow-including inclusive descendant elements, in shadow-including tree order.
Vector<GC::Root<DOM::Element>> candidates;
root->for_each_shadow_including_inclusive_descendant([&](DOM::Node& inclusive_descendant) {
if (!is<DOM::Element>(inclusive_descendant))
// 1. For each shadow-including inclusive descendant candidate of root, in shadow-including tree order:
root->for_each_shadow_including_inclusive_descendant([&](DOM::Node& candidate) {
// 1. If candidate is not an Element node, then continue.
auto* element = as_if<DOM::Element>(candidate);
if (!element)
return TraversalDecision::Continue;
auto& inclusive_descendant_element = static_cast<DOM::Element&>(inclusive_descendant);
candidates.append(GC::make_root(inclusive_descendant_element));
// 2. If candidate's custom element registry is not this, then continue.
if (element->custom_element_registry() != this)
return TraversalDecision::Continue;
// 3. Try to upgrade candidate.
element->try_to_upgrade();
return TraversalDecision::Continue;
});
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-initialize
WebIDL::ExceptionOr<void> CustomElementRegistry::initialize_for_bindings(GC::Ref<DOM::Node> root)
{
// 1. If this's is scoped is false and either root is a Document node or root's node document's custom element
// registry is not this, then throw a "NotSupportedError" DOMException.
if (!is_scoped() && (root->is_document() || root->document().custom_element_registry() != this))
return WebIDL::NotSupportedError::create(realm(), "CustomElementRegistry must either be scoped or the document's custom element registry."_utf16);
// 2. If root is a Document node whose custom element registry is null, then set root's custom element registry to
// this.
if (auto* document = as_if<DOM::Document>(*root); document && !document->custom_element_registry())
document->set_custom_element_registry(this);
// 3. Otherwise, if root is a ShadowRoot node whose custom element registry is null, then set root's custom element
// registry to this.
else if (auto* shadow_root = as_if<DOM::ShadowRoot>(*root); shadow_root && !shadow_root->custom_element_registry())
shadow_root->set_custom_element_registry(this);
// 4. For each inclusive descendant inclusiveDescendant of root, in tree order:
root->for_each_in_inclusive_subtree([this](auto& inclusive_descendant) {
// 1. If inclusiveDescendant is not an Element node, then continue.
auto* element = as_if<DOM::Element>(inclusive_descendant);
if (!element)
return TraversalDecision::Continue;
// 2. If inclusiveDescendant's custom element registry is null:
if (!element->custom_element_registry()) {
// 1. Set inclusiveDescendant's custom element registry to this.
element->set_custom_element_registry(this);
// 2. If this's is scoped is true, then append inclusiveDescendant's node document to this's scoped
// document set.
if (m_is_scoped)
append_scoped_document(element->document());
}
// 3. If inclusiveDescendant's custom element registry is not this, then continue.
if (element->custom_element_registry() != this)
return TraversalDecision::Continue;
// 4. Try to upgrade inclusiveDescendant.
element->try_to_upgrade();
return TraversalDecision::Continue;
});
// 2. For each candidate of candidates, try to upgrade candidate.
for (auto& candidate : candidates)
candidate->try_to_upgrade();
return {};
}
void CustomElementRegistry::append_scoped_document(GC::Ref<DOM::Document> document)
{
m_scoped_documents.set(document);
}
GC::Ptr<CustomElementDefinition> CustomElementRegistry::get_definition_with_name_and_local_name(String const& name, String const& local_name) const
@@ -426,6 +482,26 @@ GC::Ptr<CustomElementDefinition> CustomElementRegistry::get_definition_from_new_
return definition_iterator.is_end() ? nullptr : definition_iterator->ptr();
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#look-up-a-custom-element-registry
GC::Ptr<CustomElementRegistry> look_up_a_custom_element_registry(DOM::Node const& node)
{
// To look up a custom element registry, given a Node object node:
// 1. If node is an Element object, then return node's custom element registry.
if (auto* element = as_if<DOM::Element>(node))
return element->custom_element_registry();
// 2. If node is a ShadowRoot object, then return node's custom element registry.
if (auto* shadow_root = as_if<DOM::ShadowRoot>(node))
return shadow_root->custom_element_registry();
// 3. If node is a Document object, then return node's custom element registry.
if (auto* document = as_if<DOM::Document>(node))
return document->custom_element_registry();
// 4. Return null.
return nullptr;
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#look-up-a-custom-element-definition
GC::Ptr<CustomElementDefinition> look_up_a_custom_element_definition(GC::Ptr<CustomElementRegistry> registry, Optional<FlyString> const& namespace_, FlyString const& local_name, Optional<String> const& is)
{
@@ -455,4 +531,12 @@ GC::Ptr<CustomElementDefinition> look_up_a_custom_element_definition(GC::Ptr<Cus
return registry->get_definition_with_name_and_local_name(is.value(), converted_local_name);
}
// https://dom.spec.whatwg.org/#is-a-global-custom-element-registry
bool is_a_global_custom_element_registry(GC::Ptr<CustomElementRegistry> registry)
{
// Null or a CustomElementRegistry object registry is a global custom element registry if registry is non-null and
// registrys is scoped is false.
return registry && !registry->is_scoped();
}
}