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
parent 97768d8927
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

@ -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 };
}