mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-26 17:55:07 +02:00
LibWeb: Implement scoped custom element registries
This commit is contained in:
committed by
Tim Ledbetter
parent
90c39c214d
commit
ed6a5f25a0
Notes:
github-actions[bot]
2026-03-27 19:50:58 +00:00
Author: https://github.com/AtkinsSJ Commit: https://github.com/LadybirdBrowser/ladybird/commit/ed6a5f25a0a Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/8613 Reviewed-by: https://github.com/tcl3 ✅
@@ -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
|
||||
// registry’s is scoped is false.
|
||||
return registry && !registry->is_scoped();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user