ladybird/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp

542 lines
27 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/CustomElementRegistryPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/HTML/CustomElements/CustomElementName.h>
#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Namespace.h>
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)
{
}
CustomElementRegistry::~CustomElementRegistry() = default;
void CustomElementRegistry::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(CustomElementRegistry);
Base::initialize(realm);
}
void CustomElementRegistry::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_custom_element_definitions);
visitor.visit(m_when_defined_promise_map);
}
// https://webidl.spec.whatwg.org/#es-callback-function
// https://github.com/whatwg/html/pull/9893
static JS::ThrowCompletionOr<GC::Ref<WebIDL::CallbackType>> convert_value_to_callback_function(JS::VM& vm, JS::Value value)
{
// FIXME: De-duplicate this from the IDL generator.
// 1. If the result of calling IsCallable(V) is false and the conversion to an IDL value is not being performed due to V being assigned to an attribute whose type is a nullable callback function that is annotated with [LegacyTreatNonObjectAsNull], then throw a TypeError.
if (!value.is_function())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAFunction, value);
// 2. Return the IDL callback function type value that represents a reference to the same object that V represents, with the incumbent realm as the callback context.
return vm.heap().allocate<WebIDL::CallbackType>(value.as_object(), HTML::incumbent_realm());
}
// https://webidl.spec.whatwg.org/#es-sequence
static JS::ThrowCompletionOr<Vector<String>> convert_value_to_sequence_of_strings(JS::VM& vm, JS::Value value)
{
// FIXME: De-duplicate this from the IDL generator.
// An ECMAScript value V is converted to an IDL sequence<T> value as follows:
// 1. If V is not an Object, throw a TypeError.
if (!value.is_object())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, value);
// 2. Let method be ? GetMethod(V, @@iterator).
auto method = TRY(value.get_method(vm, vm.well_known_symbol_iterator()));
// 3. If method is undefined, throw a TypeError.
if (!method)
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, value);
// 4. Return the result of creating a sequence from V and method.
// https://webidl.spec.whatwg.org/#create-sequence-from-iterable
// To create an IDL value of type sequence<T> given an iterable iterable and an iterator getter method, perform the following steps:
// 1. Let iter be ? GetIterator(iterable, sync, method).
// FIXME: The WebIDL spec is out of date - it should be using GetIteratorFromMethod.
auto iterator = TRY(JS::get_iterator_from_method(vm, value, *method));
// 2. Initialize i to be 0.
Vector<String> sequence_of_strings;
// 3. Repeat
for (;;) {
// 1. Let next be ? IteratorStep(iter).
auto next = TRY(JS::iterator_step(vm, iterator));
// 2. If next is false, then return an IDL sequence value of type sequence<T> of length i, where the value of the element at index j is Sj.
if (!next.has<JS::IterationResult>())
return sequence_of_strings;
// 3. Let nextItem be ? IteratorValue(next).
auto next_item = TRY(next.get<JS::IterationResult>().value);
// 4. Initialize Si to the result of converting nextItem to an IDL value of type T.
// https://webidl.spec.whatwg.org/#es-DOMString
// An ECMAScript value V is converted to an IDL DOMString value by running the following algorithm:
// 1. If V is null and the conversion is to an IDL type associated with the [LegacyNullToEmptyString] extended attribute, then return the DOMString value that represents the empty string.
// NOTE: This doesn't apply.
// 2. Let x be ? ToString(V).
// 3. Return the IDL DOMString value that represents the same sequence of code units as the one the ECMAScript String value x represents.
auto string_value = TRY(next_item.to_string(vm));
sequence_of_strings.append(move(string_value));
// 5. Set i to i + 1.
}
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define
JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, WebIDL::CallbackType* constructor, ElementDefinitionOptions options)
{
auto& realm = this->realm();
auto& vm = this->vm();
// 1. If IsConstructor(constructor) is false, then throw a TypeError.
if (!JS::Value(constructor->callback).is_constructor())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAConstructor, JS::Value(constructor->callback));
// 2. If name is not a valid custom element name, then throw a "SyntaxError" DOMException.
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.
auto existing_definition_with_name_iterator = m_custom_element_definitions.find_if([&name](auto const& definition) {
return definition->name() == name;
});
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.
auto existing_definition_with_constructor_iterator = m_custom_element_definitions.find_if([&constructor](auto const& definition) {
return definition->constructor().callback == constructor->callback;
});
if (existing_definition_with_constructor_iterator != m_custom_element_definitions.end())
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "The given constructor is already in use by another custom element"_utf16));
// 5. Let localName be name.
String local_name = name;
// 6. Let extends be options["extends"] if it exists; otherwise null.
auto& extends = options.extends;
// 7. If extends is not null:
if (extends.has_value()) {
// 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())));
// 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())));
// 4. Set localName to extends.
local_name = extends.value();
}
// 8. If this's element definition is running is true, then throw a "NotSupportedError" DOMException.
if (m_element_definition_is_running)
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Cannot recursively define custom elements"_utf16));
// 9. Set this's element definition is running to true.
m_element_definition_is_running = true;
// 10. Let formAssociated be false.
bool form_associated = false;
// 11. Let disableInternals be false.
bool disable_internals = false;
// 12. Let disableShadow be false.
bool disable_shadow = false;
// 13. Let observedAttributes be an empty sequence<DOMString>.
Vector<String> observed_attributes;
// 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));
// 2. If prototype is not an Object, then throw a TypeError exception.
auto prototype = prototype_value.as_if<JS::Object>();
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 ]».
lifecycle_callbacks.set(CustomElementReactionNames::connectedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::disconnectedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::adoptedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::connectedMoveCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::attributeChangedCallback, {});
// 4. For each callbackName of the keys of lifecycleCallbacks:
for (auto const& callback_name : { CustomElementReactionNames::connectedCallback, CustomElementReactionNames::disconnectedCallback, CustomElementReactionNames::adoptedCallback, CustomElementReactionNames::connectedMoveCallback, CustomElementReactionNames::attributeChangedCallback }) {
// 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.
if (!callback_value.is_undefined()) {
auto callback = TRY(convert_value_to_callback_function(vm, callback_value));
lifecycle_callbacks.set(callback_name, callback);
}
}
// 5. If lifecycleCallbacks["attributeChangedCallback"] is not null:
auto attribute_changed_callback_iterator = lifecycle_callbacks.find(CustomElementReactionNames::attributeChangedCallback);
VERIFY(attribute_changed_callback_iterator != lifecycle_callbacks.end());
if (attribute_changed_callback_iterator->value) {
// 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.
if (!observed_attributes_iterable.is_undefined())
observed_attributes = TRY(convert_value_to_sequence_of_strings(vm, observed_attributes_iterable));
}
// 6. Let disabledFeatures be an empty sequence<DOMString>.
Vector<String> disabled_features;
// 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.
if (!disabled_features_iterable.is_undefined())
disabled_features = TRY(convert_value_to_sequence_of_strings(vm, disabled_features_iterable));
// 9. If disabledFeatures contains "internals", then set disableInternals to true.
disable_internals = disabled_features.contains_slow("internals"sv);
// 10. If disabledFeatures contains "shadow", then set disableShadow to true.
disable_shadow = disabled_features.contains_slow("shadow"sv);
// 11. Let formAssociatedValue be ? Get( constructor, "formAssociated").
auto form_associated_value = TRY(constructor->callback->get(vm.names.formAssociated));
// 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" »:
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.
if (!callback_value.is_undefined())
lifecycle_callbacks.set(callback_name, TRY(convert_value_to_callback_function(vm, callback_value)));
}
}
return {};
};
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.
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.
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. 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);
}
// 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.
WebIDL::resolve_promise(realm, promise_when_defined_iterator->value, constructor->callback);
// 2. Remove this's when-defined promise map[name].
m_when_defined_promise_map.remove(name);
}
return {};
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-get
Variant<GC::Root<WebIDL::CallbackType>, Empty> CustomElementRegistry::get(String const& name) const
{
// 1. If this's custom element definition set contains an item with name name, then return that item's constructor.
auto existing_definition_iterator = m_custom_element_definitions.find_if([&name](auto const& definition) {
return definition->name() == name;
});
if (!existing_definition_iterator.is_end())
return GC::make_root((*existing_definition_iterator)->constructor());
// 2. Return undefined.
return Empty {};
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-getname
Optional<String> CustomElementRegistry::get_name(GC::Root<WebIDL::CallbackType> const& constructor) const
{
// 1. If this's custom element definition set contains an item with constructor constructor, then return that item's name.
auto existing_definition_iterator = m_custom_element_definitions.find_if([&constructor](auto const& definition) {
return definition->constructor().callback == constructor.cell()->callback;
});
if (!existing_definition_iterator.is_end())
return (*existing_definition_iterator)->name();
// 2. Return null.
return {};
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-whendefined
WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> CustomElementRegistry::when_defined(String const& name)
{
auto& realm = this->realm();
// 1. If name is not a valid custom element name, then return a promise rejected with a "SyntaxError" DOMException.
if (!is_valid_custom_element_name(name))
return WebIDL::create_rejected_promise(realm, WebIDL::SyntaxError::create(realm, Utf16String::formatted("'{}' is not a valid custom element name", name)));
// 2. If this's custom element definition set contains an item with name name, then return a promise resolved with that item's constructor.
auto existing_definition_iterator = m_custom_element_definitions.find_if([&name](auto const& definition) {
return definition->name() == name;
});
if (existing_definition_iterator != m_custom_element_definitions.end())
return WebIDL::create_resolved_promise(realm, (*existing_definition_iterator)->constructor().callback);
// 3. If this's when-defined promise map[name] does not exist, then set this's when-defined promise map[name] to a new promise.
auto existing_promise_iterator = m_when_defined_promise_map.find(name);
GC::Ptr<WebIDL::Promise> promise;
if (existing_promise_iterator == m_when_defined_promise_map.end()) {
promise = WebIDL::create_promise(realm);
m_when_defined_promise_map.set(name, *promise);
} else {
promise = existing_promise_iterator->value;
}
// 4. Return this's when-defined promise map[name].
VERIFY(promise);
return GC::Ref { *promise };
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-upgrade
void CustomElementRegistry::upgrade(GC::Ref<DOM::Node> root) const
{
// 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;
// 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;
});
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
{
auto definition_iterator = m_custom_element_definitions.find_if([&](auto const& definition) {
return definition->name() == name && definition->local_name() == local_name;
});
return definition_iterator.is_end() ? nullptr : definition_iterator->ptr();
}
GC::Ptr<CustomElementDefinition> CustomElementRegistry::get_definition_from_new_target(JS::FunctionObject const& new_target) const
{
auto definition_iterator = m_custom_element_definitions.find_if([&](auto const& definition) {
return definition->constructor().callback.ptr() == &new_target;
});
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)
{
// 1. If registry is null, then return null.
if (!registry)
return nullptr;
// 2. If namespace is not the HTML namespace, then return null.
if (namespace_ != Namespace::HTML)
return nullptr;
// 3. If registry's custom element definition set contains an item with name and local name both equal to
// localName, then return that item.
auto converted_local_name = local_name.to_string();
if (auto maybe_definition = registry->get_definition_with_name_and_local_name(converted_local_name, converted_local_name))
return maybe_definition;
// 4. If registry's custom element definition set contains an item with name equal to is and local name equal to
// localName, then return that item.
// 5. Return null.
// NB: If `is` has no value, it can never match as custom element definitions always have a name and localName
// (i.e. not stored as Optional<String>)
if (!is.has_value())
return nullptr;
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();
}
}