LibWeb: Match spec changes for "custom element registry" concept

Corresponds to https://github.com/whatwg/html/pull/10845 and
https://github.com/whatwg/html/pull/10865
This commit is contained in:
Sam Atkins 2024-12-18 15:58:36 +00:00
commit 5651c6fd9b
Notes: github-actions[bot] 2024-12-18 19:24:00 +00:00
4 changed files with 72 additions and 73 deletions

View file

@ -3156,31 +3156,31 @@ void Document::set_window(HTML::Window& window)
// https://html.spec.whatwg.org/multipage/custom-elements.html#look-up-a-custom-element-definition
GC::Ptr<HTML::CustomElementDefinition> Document::lookup_custom_element_definition(Optional<FlyString> const& namespace_, FlyString const& local_name, Optional<String> const& is) const
{
// 1. If namespace is not the HTML namespace, return null.
// 1. If namespace is not the HTML namespace, then return null.
if (namespace_ != Namespace::HTML)
return nullptr;
// 2. If document's browsing context is null, return null.
// 2. If document's browsing context is null, then return null.
if (!browsing_context())
return nullptr;
// 3. Let registry be document's relevant global object's CustomElementRegistry object.
// 3. Let registry be document's relevant global object's custom element registry.
auto registry = verify_cast<HTML::Window>(relevant_global_object(*this)).custom_elements();
// 4. If there is custom element definition in registry with name and local name both equal to localName, return that custom element definition.
auto converted_local_name = local_name;
auto maybe_definition = registry->get_definition_with_name_and_local_name(converted_local_name.to_string(), converted_local_name.to_string());
// 4. 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();
auto maybe_definition = registry->get_definition_with_name_and_local_name(converted_local_name, converted_local_name);
if (maybe_definition)
return maybe_definition;
// 5. If there is a custom element definition in registry with name equal to is and local name equal to localName, return that custom element definition.
// 5. 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.
// 6. Return null.
// NOTE: 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.to_string());
return registry->get_definition_with_name_and_local_name(is.value(), converted_local_name);
}
CSS::StyleSheetList& Document::style_sheets()

View file

@ -126,7 +126,7 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
if (!is_valid_custom_element_name(name))
return JS::throw_completion(WebIDL::SyntaxError::create(realm, MUST(String::formatted("'{}' is not a valid custom element name"sv, name))));
// 3. If this CustomElementRegistry contains an entry 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 +134,7 @@ 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, MUST(String::formatted("A custom element with name '{}' is already defined"sv, name))));
// 4. If this CustomElementRegistry contains an entry 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;
});
@ -145,16 +145,18 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
// 5. Let localName be name.
String local_name = name;
// 6. Let extends be the value of the extends member of options, or null if no such member exists.
// 6. Let extends be options["extends"] if it exists; otherwise null.
auto& extends = options.extends;
// 7. If extends is not null, then:
// 7. If extends is not null:
if (extends.has_value()) {
// 1. 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, MUST(String::formatted("'{}' is a custom element name, only non-custom elements can be extended"sv, 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.
// 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.
if (DOM::is_unknown_html_element(extends.value()))
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, MUST(String::formatted("'{}' is an unknown HTML element"sv, extends.value()))));
@ -162,11 +164,11 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
local_name = extends.value();
}
// 8. If this CustomElementRegistry's element definition is running flag is set, then throw a "NotSupportedError" DOMException.
// 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"_string));
// 9. Set this CustomElementRegistry's element definition is running flag.
// 9. Set this's element definition is running to true.
m_element_definition_is_running = true;
// 10. Let formAssociated be false.
@ -181,10 +183,11 @@ 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.
// 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.
CustomElementDefinition::LifecycleCallbacksStorage lifecycle_callbacks;
// 14. Run the following substeps while catching any exceptions:
// 14. Run the following steps while catching any exceptions:
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));
@ -195,32 +198,35 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
auto& prototype = prototype_value.as_object();
// 3. Let lifecycleCallbacks be a map with the keys "connectedCallback", "disconnectedCallback", "adoptedCallback", and "attributeChangedCallback", each of which belongs to an entry whose value is null.
// 3. Let lifecycleCallbacks be the ordered map «[ "connectedCallback" → null, "disconnectedCallback" → null, "adoptedCallback" → null,
// "attributeChangedCallback" → null ]».
lifecycle_callbacks.set(CustomElementReactionNames::connectedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::disconnectedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::adoptedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::attributeChangedCallback, {});
// 4. For each of the keys callbackName in lifecycleCallbacks, in the order listed in the previous step:
// 4. For each callbackName of the keys of lifecycleCallbacks:
for (auto const& callback_name : { CustomElementReactionNames::connectedCallback, CustomElementReactionNames::disconnectedCallback, CustomElementReactionNames::adoptedCallback, CustomElementReactionNames::attributeChangedCallback }) {
// 1. Let callbackValue be ? Get(prototype, callbackName).
auto callback_value = TRY(prototype.get(callback_name.to_deprecated_fly_string()));
// 2. If callbackValue is not undefined, then set the value of the entry in lifecycleCallbacks with key callbackName to the result of converting callbackValue to the Web IDL Function callback type. Rethrow any exceptions from the conversion.
// 2. If callbackValue is not undefined, then set the value of the entry in lifecycleCallbacks with key 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 the value of the entry in lifecycleCallbacks with key "attributeChangedCallback" is not null, then:
// 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.
// 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));
}
@ -231,35 +237,32 @@ 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));
// 9. Set disableInternals to true if disabledFeatures contains "internals".
// 9. If disabledFeatures contains "internals", then set disableInternals to true.
disable_internals = disabled_features.contains_slow("internals"sv);
// 10. Set disableShadow to true if disabledFeatures contains "shadow".
// 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. Rethrow any exceptions from the conversion.
// NOTE: Converting to a boolean cannot throw with ECMAScript.
// https://webidl.spec.whatwg.org/#es-boolean
// An ECMAScript value V is converted to an IDL boolean value by running the following algorithm:
// 1. Let x be the result of computing ToBoolean(V).
// 2. Return the IDL boolean value that is the one that represents the same truth value as the ECMAScript Boolean value x.
// 12. Set formAssociated to the result of converting formAssociatedValue to a boolean.
form_associated = form_associated_value.to_boolean();
// 13. If formAssociated is true, for each of "formAssociatedCallback", "formResetCallback", "formDisabledCallback", and "formStateRestoreCallback" callbackName:
// 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(callback_name.to_deprecated_fly_string()));
// 2. If callbackValue is not undefined, then set the value of the entry in lifecycleCallbacks with key callbackName to the result of converting callbackValue to the Web IDL Function callback type. Rethrow any exceptions from the conversion.
// 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)));
}
@ -270,24 +273,26 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
auto maybe_exception = get_definition_attributes_from_constructor();
// Then, perform the following substep, regardless of whether the above steps threw an exception or not:
// 1. Unset this CustomElementRegistry's element definition is running flag.
// 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 first set of substeps threw an exception, then rethrow that exception (thus terminating this algorithm). Otherwise, continue onward.
// 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. Add definition to this CustomElementRegistry.
// 16. Append definition to this's custom element definition set.
m_custom_element_definitions.append(definition);
// 17. Let document be this CustomElementRegistry's relevant global object's associated Document.
// 17. Let document be this's relevant global object's associated Document.
auto& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document();
// 18. Let upgrade candidates 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.
// 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;
@ -303,20 +308,17 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
return TraversalDecision::Continue;
});
// 19. For each element element in upgrade candidates, enqueue a custom element upgrade reaction given element and definition.
// 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 CustomElementRegistry's when-defined promise map contains an entry with key name:
// 20. 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. Let promise be the value of that entry.
auto promise = promise_when_defined_iterator->value;
// 1. Resolve this's when-defined promise map[name] with constructor.
WebIDL::resolve_promise(realm, promise_when_defined_iterator->value, constructor->callback);
// 2. Resolve promise with constructor.
WebIDL::resolve_promise(realm, promise, constructor->callback);
// 3. Delete the entry with key name from this CustomElementRegistry's when-defined promise map.
// 2. Remove this's when-defined promise map[name].
m_when_defined_promise_map.remove(name);
}
@ -326,7 +328,7 @@ JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, We
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-get
Variant<GC::Root<WebIDL::CallbackType>, JS::Value> CustomElementRegistry::get(String const& name) const
{
// 1. If this CustomElementRegistry contains an entry with name name, then return that entry's constructor.
// 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;
});
@ -334,14 +336,14 @@ Variant<GC::Root<WebIDL::CallbackType>, JS::Value> CustomElementRegistry::get(St
if (!existing_definition_iterator.is_end())
return GC::make_root((*existing_definition_iterator)->constructor());
// 2. Otherwise, return undefined.
// 2. Return undefined.
return JS::js_undefined();
}
// 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 CustomElementRegistry contains an entry with constructor constructor, then return that entry's name.
// 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;
});
@ -358,11 +360,11 @@ WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> CustomElementRegistry::when_define
{
auto& realm = this->realm();
// 1. If name is not a valid custom element name, then return a new promise rejected with a "SyntaxError" DOMException.
// 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, MUST(String::formatted("'{}' is not a valid custom element name"sv, name))));
// 2. If this CustomElementRegistry contains an entry with name name, then return a new promise resolved with that entry's constructor.
// 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](GC::Root<CustomElementDefinition> const& definition) {
return definition->name() == name;
});
@ -370,22 +372,17 @@ WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> CustomElementRegistry::when_define
if (existing_definition_iterator != m_custom_element_definitions.end())
return WebIDL::create_resolved_promise(realm, (*existing_definition_iterator)->constructor().callback);
// 3. Let map be this CustomElementRegistry's when-defined promise map.
// NOTE: Not necessary.
// 4. If map does not contain an entry with key name, create an entry in map with key name and whose value is a new promise.
// 5. Let promise be the value of the entry in map with key name.
GC::Ptr<WebIDL::Promise> promise;
// 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);
if (existing_promise_iterator != m_when_defined_promise_map.end()) {
promise = existing_promise_iterator->value;
} else {
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;
}
// 5. Return promise.
// 4. Return this's when-defined promise map[name].
VERIFY(promise);
return GC::Ref { *promise };
}

View file

@ -310,7 +310,8 @@ private:
GC::Ptr<Navigation> m_navigation;
// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-api
// Each Window object is associated with a unique instance of a CustomElementRegistry object, allocated when the Window object is created.
// Each Window object has an associated custom element registry (a CustomElementRegistry object).
// It is set to a new CustomElementRegistry object when the Window object is created.
GC::Ptr<CustomElementRegistry> m_custom_element_registry;
GC::Ptr<AnimationFrameCallbackDriver> m_animation_frame_callback_driver;

View file

@ -2523,22 +2523,23 @@ static void generate_html_constructor(SourceGenerator& generator, IDL::Construct
constructor_generator.append(R"~~~(
auto& window = verify_cast<HTML::Window>(HTML::current_principal_global_object());
// 1. Let registry be the current global object's CustomElementRegistry object.
// 1. Let registry be current global object's custom element registry.
auto registry = TRY(throw_dom_exception_if_needed(vm, [&] { return window.custom_elements(); }));
// 2. If NewTarget is equal to the active function object, then throw a TypeError.
if (&new_target == vm.active_function_object())
return vm.throw_completion<JS::TypeError>("Cannot directly construct an HTML element, it must be inherited"sv);
// 3. Let definition be the entry in registry with constructor equal to NewTarget. If there is no such definition, then throw a TypeError.
// 3. Let definition be the item in registry's custom element definition set with constructor equal to NewTarget.
// If there is no such item, then throw a TypeError.
auto definition = registry->get_definition_from_new_target(new_target);
if (!definition)
return vm.throw_completion<JS::TypeError>("There is no custom element definition assigned to the given constructor"sv);
// 4. Let is value be null.
// 4. Let isValue be null.
Optional<String> is_value;
// 5. If definition's local name is equal to definition's name (i.e., definition is for an autonomous custom element), then:
// 5. If definition's local name is equal to definition's name (i.e., definition is for an autonomous custom element):
if (definition->local_name() == definition->name()) {
// 1. If the active function object is not HTMLElement, then throw a TypeError.
)~~~");
@ -2565,11 +2566,11 @@ static void generate_html_constructor(SourceGenerator& generator, IDL::Construct
if (!valid_local_names.contains_slow(definition->local_name()))
return vm.throw_completion<JS::TypeError>(MUST(String::formatted("Local name '{}' of customized built-in element is not a valid local name for @name@"sv, definition->local_name())));
// 3. Set is value to definition's name.
// 3. Set isValue to definition's name.
is_value = definition->name();
}
// 7. If definition's construction stack is empty, then:
// 7. If definition's construction stack is empty:
if (definition->construction_stack().is_empty()) {
// 1. Let element be the result of internally creating a new object implementing the interface to which the active function object corresponds, given the current Realm Record and NewTarget.
// 2. Set element's node document to the current global object's associated Document.
@ -2599,7 +2600,7 @@ static void generate_html_constructor(SourceGenerator& generator, IDL::Construct
// 6. Set element's custom element state to "custom".
// 7. Set element's custom element definition to definition.
// 8. Set element's is value to is value.
// 8. Set element's is value to isValue.
element->setup_custom_element_from_constructor(*definition, is_value);
// 9. Return element.