/* * Copyright (c) 2023, Luke Wilde * Copyright (c) 2025, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include 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::construct_impl(JS::Realm& realm) { // The new CustomElementRegistry() constructor steps are to set this's is scoped to true. auto registry = realm.create(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> 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::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(value.as_object(), HTML::incumbent_realm()); } // https://webidl.spec.whatwg.org/#es-sequence static JS::ThrowCompletionOr> 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 value as follows: // 1. If V is not an Object, throw a TypeError. if (!value.is_object()) return vm.throw_completion(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::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 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 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 of length i, where the value of the element at index j is Sj. if (!next.has()) return sequence_of_strings; // 3. Let nextItem be ? IteratorValue(next). auto next_item = TRY(next.get().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 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::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. Vector observed_attributes; // 14. Run the following steps while catching any exceptions: OrderedHashMap> lifecycle_callbacks; auto get_definition_attributes_from_constructor = [&]() -> JS::ThrowCompletionOr { // 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(); if (!prototype) return vm.throw_completion(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. 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. Vector 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. 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(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, 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 CustomElementRegistry::get_name(GC::Root 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> 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 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 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(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 CustomElementRegistry::initialize_for_bindings(GC::Ref 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(*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(*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(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 document) { m_scoped_documents.set(document); } GC::Ptr 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 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 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(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(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(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 look_up_a_custom_element_definition(GC::Ptr registry, Optional const& namespace_, FlyString const& local_name, Optional 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) 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 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(); } }