summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Wilde <lukew@serenityos.org>2023-03-29 23:46:18 +0100
committerAndreas Kling <kling@serenityos.org>2023-04-06 11:36:56 +0200
commit034aaf3f51e4779f03eaa5becb016e04a4ff9f4d (patch)
tree987344660dc313c74a1d7fbd57cf31567357ccf9
parent083b547e97fbba26c9db43d1b5f5e6d3ac15d617 (diff)
downloadserenity-034aaf3f51e4779f03eaa5becb016e04a4ff9f4d.zip
LibWeb: Introduce CustomElementRegistry and creating custom elements
The main missing feature here is form associated custom elements.
-rw-r--r--Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp43
-rw-r--r--Userland/Libraries/LibWeb/Bindings/MainThreadVM.h32
-rw-r--r--Userland/Libraries/LibWeb/CMakeLists.txt2
-rw-r--r--Userland/Libraries/LibWeb/DOM/Attr.cpp30
-rw-r--r--Userland/Libraries/LibWeb/DOM/Attr.h13
-rw-r--r--Userland/Libraries/LibWeb/DOM/DOMImplementation.cpp2
-rw-r--r--Userland/Libraries/LibWeb/DOM/Document.cpp122
-rw-r--r--Userland/Libraries/LibWeb/DOM/Document.h13
-rw-r--r--Userland/Libraries/LibWeb/DOM/Document.idl7
-rw-r--r--Userland/Libraries/LibWeb/DOM/Element.cpp243
-rw-r--r--Userland/Libraries/LibWeb/DOM/Element.h59
-rw-r--r--Userland/Libraries/LibWeb/DOM/ElementFactory.cpp393
-rw-r--r--Userland/Libraries/LibWeb/DOM/ElementFactory.h7
-rw-r--r--Userland/Libraries/LibWeb/DOM/Node.cpp59
-rw-r--r--Userland/Libraries/LibWeb/DOM/Node.h4
-rw-r--r--Userland/Libraries/LibWeb/DOM/QualifiedName.cpp5
-rw-r--r--Userland/Libraries/LibWeb/DOM/QualifiedName.h2
-rw-r--r--Userland/Libraries/LibWeb/DOM/ShadowRoot.h22
-rw-r--r--Userland/Libraries/LibWeb/Forward.h2
-rw-r--r--Userland/Libraries/LibWeb/HTML/AttributeNames.h1
-rw-r--r--Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp8
-rw-r--r--Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementDefinition.h111
-rw-r--r--Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.cpp29
-rw-r--r--Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.h30
-rw-r--r--Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp411
-rw-r--r--Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.h51
-rw-r--r--Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.idl15
-rw-r--r--Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp4
-rw-r--r--Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp85
-rw-r--r--Userland/Libraries/LibWeb/HTML/Parser/HTMLToken.h2
-rw-r--r--Userland/Libraries/LibWeb/HTML/Scripting/Environments.cpp7
-rw-r--r--Userland/Libraries/LibWeb/HTML/Scripting/Environments.h1
-rw-r--r--Userland/Libraries/LibWeb/HTML/TagNames.h4
-rw-r--r--Userland/Libraries/LibWeb/HTML/Window.cpp13
-rw-r--r--Userland/Libraries/LibWeb/HTML/Window.h6
-rw-r--r--Userland/Libraries/LibWeb/HTML/Window.idl2
-rw-r--r--Userland/Libraries/LibWeb/Loader/FrameLoader.cpp21
-rw-r--r--Userland/Libraries/LibWeb/idl_files.cmake1
38 files changed, 1733 insertions, 129 deletions
diff --git a/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp b/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp
index 9bbdc7a759..b21f308834 100644
--- a/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp
+++ b/Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org>
- * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
+ * Copyright (c) 2021-2023, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2022, networkException <networkexception@serenityos.org>
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
@@ -21,6 +21,8 @@
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/MutationType.h>
#include <LibWeb/HTML/AttributeNames.h>
+#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
+#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/Location.h>
#include <LibWeb/HTML/PromiseRejectionEvent.h>
@@ -78,6 +80,7 @@ ErrorOr<void> initialize_main_thread_vm()
// These strings could potentially live on the VM similar to CommonPropertyNames.
TRY(DOM::MutationType::initialize_strings());
TRY(HTML::AttributeNames::initialize_strings());
+ TRY(HTML::CustomElementReactionNames::initialize_strings());
TRY(HTML::EventNames::initialize_strings());
TRY(HTML::TagNames::initialize_strings());
TRY(Namespace::initialize_strings());
@@ -499,4 +502,42 @@ void WebEngineCustomData::spin_event_loop_until(Function<bool()> goal_condition)
Platform::EventLoopPlugin::the().spin_until(move(goal_condition));
}
+// https://html.spec.whatwg.org/multipage/custom-elements.html#invoke-custom-element-reactions
+void invoke_custom_element_reactions(Vector<JS::Handle<DOM::Element>>& element_queue)
+{
+ // 1. While queue is not empty:
+ while (!element_queue.is_empty()) {
+ // 1. Let element be the result of dequeuing from queue.
+ auto element = element_queue.take_first();
+
+ // 2. Let reactions be element's custom element reaction queue.
+ auto& reactions = element->custom_element_reaction_queue();
+
+ // 3. Repeat until reactions is empty:
+ while (!reactions.is_empty()) {
+ // 1. Remove the first element of reactions, and let reaction be that element. Switch on reaction's type:
+ auto reaction = reactions.take_first();
+
+ auto maybe_exception = reaction.visit(
+ [&](DOM::CustomElementUpgradeReaction const& custom_element_upgrade_reaction) -> JS::ThrowCompletionOr<void> {
+ // -> upgrade reaction
+ // Upgrade element using reaction's custom element definition.
+ return element->upgrade_element(*custom_element_upgrade_reaction.custom_element_definition);
+ },
+ [&](DOM::CustomElementCallbackReaction& custom_element_callback_reaction) -> JS::ThrowCompletionOr<void> {
+ // -> callback reaction
+ // Invoke reaction's callback function with reaction's arguments, and with element as the callback this value.
+ auto result = WebIDL::invoke_callback(*custom_element_callback_reaction.callback, element.ptr(), custom_element_callback_reaction.arguments);
+ if (result.is_abrupt())
+ return result.release_error();
+ return {};
+ });
+
+ // If this throws an exception, catch it, and report the exception.
+ if (maybe_exception.is_throw_completion())
+ HTML::report_exception(maybe_exception, element->realm());
+ }
+ }
+}
+
}
diff --git a/Userland/Libraries/LibWeb/Bindings/MainThreadVM.h b/Userland/Libraries/LibWeb/Bindings/MainThreadVM.h
index 49b8a83d2f..149211f556 100644
--- a/Userland/Libraries/LibWeb/Bindings/MainThreadVM.h
+++ b/Userland/Libraries/LibWeb/Bindings/MainThreadVM.h
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org>
- * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
+ * Copyright (c) 2021-2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -10,11 +10,31 @@
#include <LibJS/Forward.h>
#include <LibJS/Runtime/JobCallback.h>
#include <LibJS/Runtime/VM.h>
+#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/MutationObserver.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
namespace Web::Bindings {
+// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions-stack
+struct CustomElementReactionsStack {
+ CustomElementReactionsStack() = default;
+ ~CustomElementReactionsStack() = default;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#element-queue
+ // Each item in the stack is an element queue, which is initially empty as well. Each item in an element queue is an element.
+ // (The elements are not necessarily custom yet, since this queue is used for upgrades as well.)
+ Vector<Vector<JS::Handle<DOM::Element>>> element_queue_stack;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#backup-element-queue
+ // Each custom element reactions stack has an associated backup element queue, which an initially-empty element queue.
+ Vector<JS::Handle<DOM::Element>> backup_element_queue;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#processing-the-backup-element-queue
+ // To prevent reentrancy when processing the backup element queue, each custom element reactions stack also has a processing the backup element queue flag, initially unset.
+ bool processing_the_backup_element_queue { false };
+};
+
struct WebEngineCustomData final : public JS::VM::CustomData {
virtual ~WebEngineCustomData() override = default;
@@ -34,6 +54,15 @@ struct WebEngineCustomData final : public JS::VM::CustomData {
JS::Handle<JS::Realm> internal_realm;
OwnPtr<JS::ExecutionContext> root_execution_context;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions-stack
+ // Each similar-origin window agent has a custom element reactions stack, which is initially empty.
+ CustomElementReactionsStack custom_element_reactions_stack {};
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#current-element-queue
+ // A similar-origin window agent's current element queue is the element queue at the top of its custom element reactions stack.
+ Vector<JS::Handle<DOM::Element>>& current_element_queue() { return custom_element_reactions_stack.element_queue_stack.last(); }
+ Vector<JS::Handle<DOM::Element>> const& current_element_queue() const { return custom_element_reactions_stack.element_queue_stack.last(); }
};
struct WebEngineCustomJobCallbackData final : public JS::JobCallback::CustomData {
@@ -56,5 +85,6 @@ JS::VM& main_thread_vm();
void queue_mutation_observer_microtask(DOM::Document const&);
NonnullOwnPtr<JS::ExecutionContext> create_a_new_javascript_realm(JS::VM&, Function<JS::Object*(JS::Realm&)> create_global_object, Function<JS::Object*(JS::Realm&)> create_global_this_value);
+void invoke_custom_element_reactions(Vector<JS::Handle<DOM::Element>>& element_queue);
}
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt
index c7bea9da41..3785dc2cb0 100644
--- a/Userland/Libraries/LibWeb/CMakeLists.txt
+++ b/Userland/Libraries/LibWeb/CMakeLists.txt
@@ -215,6 +215,8 @@ set(SOURCES
HTML/CrossOrigin/AbstractOperations.cpp
HTML/CrossOrigin/Reporting.cpp
HTML/CustomElements/CustomElementName.cpp
+ HTML/CustomElements/CustomElementReactionNames.cpp
+ HTML/CustomElements/CustomElementRegistry.cpp
HTML/DOMParser.cpp
HTML/DOMStringMap.cpp
HTML/ErrorEvent.cpp
diff --git a/Userland/Libraries/LibWeb/DOM/Attr.cpp b/Userland/Libraries/LibWeb/DOM/Attr.cpp
index 527a07d7cd..a8735498a8 100644
--- a/Userland/Libraries/LibWeb/DOM/Attr.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Attr.cpp
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2021, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -10,15 +11,16 @@
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/MutationType.h>
#include <LibWeb/DOM/StaticNodeList.h>
+#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
namespace Web::DOM {
-WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> Attr::create(Document& document, DeprecatedFlyString local_name, DeprecatedString value, Element const* owner_element)
+WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> Attr::create(Document& document, DeprecatedFlyString local_name, DeprecatedString value, Element* owner_element)
{
return MUST_OR_THROW_OOM(document.heap().allocate<Attr>(document.realm(), document, QualifiedName(move(local_name), {}, {}), move(value), owner_element));
}
-WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> Attr::create(Document& document, QualifiedName qualified_name, DeprecatedString value, Element const* owner_element)
+WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> Attr::create(Document& document, QualifiedName qualified_name, DeprecatedString value, Element* owner_element)
{
return MUST_OR_THROW_OOM(document.heap().allocate<Attr>(document.realm(), document, move(qualified_name), move(value), owner_element));
}
@@ -28,7 +30,7 @@ JS::NonnullGCPtr<Attr> Attr::clone(Document& document)
return *heap().allocate<Attr>(realm(), document, m_qualified_name, m_value, nullptr).release_allocated_value_but_fixme_should_propagate_errors();
}
-Attr::Attr(Document& document, QualifiedName qualified_name, DeprecatedString value, Element const* owner_element)
+Attr::Attr(Document& document, QualifiedName qualified_name, DeprecatedString value, Element* owner_element)
: Node(document, NodeType::ATTRIBUTE_NODE)
, m_qualified_name(move(qualified_name))
, m_value(move(value))
@@ -50,12 +52,17 @@ void Attr::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_owner_element.ptr());
}
+Element* Attr::owner_element()
+{
+ return m_owner_element.ptr();
+}
+
Element const* Attr::owner_element() const
{
return m_owner_element.ptr();
}
-void Attr::set_owner_element(Element const* owner_element)
+void Attr::set_owner_element(Element* owner_element)
{
m_owner_element = owner_element;
}
@@ -79,14 +86,25 @@ void Attr::set_value(DeprecatedString value)
}
// https://dom.spec.whatwg.org/#handle-attribute-changes
-void Attr::handle_attribute_changes(Element const& element, DeprecatedString const& old_value, [[maybe_unused]] DeprecatedString const& new_value)
+void Attr::handle_attribute_changes(Element& element, DeprecatedString const& old_value, DeprecatedString const& new_value)
{
// 1. Queue a mutation record of "attributes" for element with attributeā€™s local name, attributeā€™s namespace, oldValue, Ā« Ā», Ā« Ā», null, and null.
auto added_node_list = StaticNodeList::create(realm(), {}).release_value_but_fixme_should_propagate_errors();
auto removed_node_list = StaticNodeList::create(realm(), {}).release_value_but_fixme_should_propagate_errors();
element.queue_mutation_record(MutationType::attributes, local_name(), namespace_uri(), old_value, added_node_list, removed_node_list, nullptr, nullptr);
- // FIXME: 2. If element is custom, then enqueue a custom element callback reaction with element, callback name "attributeChangedCallback", and an argument list containing attributeā€™s local name, oldValue, newValue, and attributeā€™s namespace.
+ // 2. If element is custom, then enqueue a custom element callback reaction with element, callback name "attributeChangedCallback", and an argument list containing attributeā€™s local name, oldValue, newValue, and attributeā€™s namespace.
+ if (element.is_custom()) {
+ auto& vm = this->vm();
+
+ JS::MarkedVector<JS::Value> arguments { vm.heap() };
+ arguments.append(JS::PrimitiveString::create(vm, local_name()));
+ arguments.append(JS::PrimitiveString::create(vm, old_value));
+ arguments.append(JS::PrimitiveString::create(vm, new_value));
+ arguments.append(JS::PrimitiveString::create(vm, namespace_uri()));
+
+ element.enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::attributeChangedCallback, move(arguments));
+ }
// FIXME: 3. Run the attribute change steps with element, attributeā€™s local name, oldValue, newValue, and attributeā€™s namespace.
}
diff --git a/Userland/Libraries/LibWeb/DOM/Attr.h b/Userland/Libraries/LibWeb/DOM/Attr.h
index ee94fd802c..cacf2b755f 100644
--- a/Userland/Libraries/LibWeb/DOM/Attr.h
+++ b/Userland/Libraries/LibWeb/DOM/Attr.h
@@ -18,8 +18,8 @@ class Attr final : public Node {
WEB_PLATFORM_OBJECT(Attr, Node);
public:
- static WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> create(Document&, QualifiedName, DeprecatedString value = "", Element const* = nullptr);
- static WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> create(Document&, DeprecatedFlyString local_name, DeprecatedString value = "", Element const* = nullptr);
+ static WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> create(Document&, QualifiedName, DeprecatedString value = "", Element* = nullptr);
+ static WebIDL::ExceptionOr<JS::NonnullGCPtr<Attr>> create(Document&, DeprecatedFlyString local_name, DeprecatedString value = "", Element* = nullptr);
JS::NonnullGCPtr<Attr> clone(Document&);
virtual ~Attr() override = default;
@@ -34,23 +34,24 @@ public:
DeprecatedString const& value() const { return m_value; }
void set_value(DeprecatedString value);
+ Element* owner_element();
Element const* owner_element() const;
- void set_owner_element(Element const* owner_element);
+ void set_owner_element(Element* owner_element);
// Always returns true: https://dom.spec.whatwg.org/#dom-attr-specified
constexpr bool specified() const { return true; }
- void handle_attribute_changes(Element const&, DeprecatedString const& old_value, DeprecatedString const& new_value);
+ void handle_attribute_changes(Element&, DeprecatedString const& old_value, DeprecatedString const& new_value);
private:
- Attr(Document&, QualifiedName, DeprecatedString value, Element const*);
+ Attr(Document&, QualifiedName, DeprecatedString value, Element*);
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
QualifiedName m_qualified_name;
DeprecatedString m_value;
- JS::GCPtr<Element const> m_owner_element;
+ JS::GCPtr<Element> m_owner_element;
};
template<>
diff --git a/Userland/Libraries/LibWeb/DOM/DOMImplementation.cpp b/Userland/Libraries/LibWeb/DOM/DOMImplementation.cpp
index 9c911174d2..445549ded9 100644
--- a/Userland/Libraries/LibWeb/DOM/DOMImplementation.cpp
+++ b/Userland/Libraries/LibWeb/DOM/DOMImplementation.cpp
@@ -56,7 +56,7 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Document>> DOMImplementation::create_docume
JS::GCPtr<Element> element;
if (!qualified_name.is_empty())
- element = TRY(xml_document->create_element_ns(namespace_, qualified_name /* FIXME: and an empty dictionary */));
+ element = TRY(xml_document->create_element_ns(namespace_, qualified_name, ElementCreationOptions {}));
if (doctype)
TRY(xml_document->append_child(*doctype));
diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp
index 3c89549ecb..c89e93f199 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Document.cpp
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
- * Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
+ * Copyright (c) 2021-2023, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
@@ -37,6 +37,9 @@
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/AttributeNames.h>
#include <LibWeb/HTML/BrowsingContext.h>
+#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
+#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
+#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
@@ -680,7 +683,7 @@ void Document::set_title(DeprecatedString const& title)
JS::GCPtr<HTML::HTMLTitleElement> title_element = head_element->first_child_of_type<HTML::HTMLTitleElement>();
if (!title_element) {
- title_element = &static_cast<HTML::HTMLTitleElement&>(*create_element(HTML::TagNames::title).release_value());
+ title_element = &static_cast<HTML::HTMLTitleElement&>(*DOM::create_element(*this, HTML::TagNames::title, Namespace::HTML).release_value_but_fixme_should_propagate_errors());
MUST(head_element->append_child(*title_element));
}
@@ -1190,8 +1193,10 @@ JS::Value Document::run_javascript(StringView source, StringView filename)
}
// https://dom.spec.whatwg.org/#dom-document-createelement
-WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> Document::create_element(DeprecatedFlyString const& a_local_name)
+WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> Document::create_element(DeprecatedString const& a_local_name, Variant<DeprecatedString, ElementCreationOptions> const& options)
{
+ auto& vm = this->vm();
+
auto local_name = a_local_name;
// 1. If localName does not match the Name production, then throw an "InvalidCharacterError" DOMException.
@@ -1202,8 +1207,15 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> Document::create_element(Deprecat
if (document_type() == Type::HTML)
local_name = local_name.to_lowercase();
- // FIXME: 3. Let is be null.
- // FIXME: 4. If options is a dictionary and options["is"] exists, then set is to it.
+ // 3. Let is be null.
+ Optional<String> is_value;
+
+ // 4. If options is a dictionary and options["is"] exists, then set is to it.
+ if (options.has<ElementCreationOptions>()) {
+ auto const& element_creation_options = options.get<ElementCreationOptions>();
+ if (!element_creation_options.is.is_null())
+ is_value = TRY_OR_THROW_OOM(vm, String::from_utf8(element_creation_options.is));
+ }
// 5. Let namespace be the HTML namespace, if this is an HTML document or thisā€™s content type is "application/xhtml+xml"; otherwise null.
DeprecatedFlyString namespace_;
@@ -1211,22 +1223,30 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> Document::create_element(Deprecat
namespace_ = Namespace::HTML;
// 6. Return the result of creating an element given this, localName, namespace, null, is, and with the synchronous custom elements flag set.
- return TRY(DOM::create_element(*this, local_name, namespace_));
+ return TRY(DOM::create_element(*this, local_name, namespace_, {}, move(is_value), true));
}
// https://dom.spec.whatwg.org/#dom-document-createelementns
// https://dom.spec.whatwg.org/#internal-createelementns-steps
-// FIXME: This only implements step 4 of the algorithm and does not take in options.
-WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> Document::create_element_ns(DeprecatedString const& namespace_, DeprecatedString const& qualified_name)
+WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> Document::create_element_ns(DeprecatedString const& namespace_, DeprecatedString const& qualified_name, Variant<DeprecatedString, ElementCreationOptions> const& options)
{
+ auto& vm = this->vm();
+
// 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and extract.
auto extracted_qualified_name = TRY(validate_and_extract(realm(), namespace_, qualified_name));
- // FIXME: 2. Let is be null.
- // FIXME: 3. If options is a dictionary and options["is"] exists, then set is to it.
+ // 2. Let is be null.
+ Optional<String> is_value;
+
+ // 3. If options is a dictionary and options["is"] exists, then set is to it.
+ if (options.has<ElementCreationOptions>()) {
+ auto const& element_creation_options = options.get<ElementCreationOptions>();
+ if (!element_creation_options.is.is_null())
+ is_value = TRY_OR_THROW_OOM(vm, String::from_utf8(element_creation_options.is));
+ }
// 4. Return the result of creating an element given document, localName, namespace, prefix, is, and with the synchronous custom elements flag set.
- return TRY(DOM::create_element(*this, extracted_qualified_name.local_name(), extracted_qualified_name.namespace_(), extracted_qualified_name.prefix()));
+ return TRY(DOM::create_element(*this, extracted_qualified_name.local_name(), extracted_qualified_name.namespace_(), extracted_qualified_name.prefix(), move(is_value), true));
}
JS::NonnullGCPtr<DocumentFragment> Document::create_document_fragment()
@@ -1393,22 +1413,49 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Node>> Document::import_node(JS::NonnullGCP
// https://dom.spec.whatwg.org/#concept-node-adopt
void Document::adopt_node(Node& node)
{
+ // 1. Let oldDocument be nodeā€™s node document.
auto& old_document = node.document();
+
+ // 2. If nodeā€™s parent is non-null, then remove node.
if (node.parent())
node.remove();
+ // 3. If document is not oldDocument, then:
if (&old_document != this) {
- node.for_each_shadow_including_descendant([&](auto& inclusive_descendant) {
+ // 1. For each inclusiveDescendant in nodeā€™s shadow-including inclusive descendants:
+ node.for_each_shadow_including_inclusive_descendant([&](DOM::Node& inclusive_descendant) {
+ // 1. Set inclusiveDescendantā€™s node document to document.
inclusive_descendant.set_document({}, *this);
- // FIXME: If inclusiveDescendant is an element, then set the node document of each attribute in inclusiveDescendantā€™s attribute list to document.
+
+ // FIXME: 2. If inclusiveDescendant is an element, then set the node document of each attribute in inclusiveDescendantā€™s
+ // attribute list to document.
return IterationDecision::Continue;
});
- // FIXME: For each inclusiveDescendant in nodeā€™s shadow-including inclusive descendants that is custom,
- // enqueue a custom element callback reaction with inclusiveDescendant, callback name "adoptedCallback",
- // and an argument list containing oldDocument and document.
+ // 2. For each inclusiveDescendant in nodeā€™s shadow-including inclusive descendants that is custom,
+ // enqueue a custom element callback reaction with inclusiveDescendant, callback name "adoptedCallback",
+ // and an argument list containing oldDocument and document.
+ node.for_each_shadow_including_inclusive_descendant([&](DOM::Node& inclusive_descendant) {
+ if (!is<DOM::Element>(inclusive_descendant))
+ return IterationDecision::Continue;
+
+ auto& element = static_cast<DOM::Element&>(inclusive_descendant);
+ if (element.is_custom()) {
+ auto& vm = this->vm();
- node.for_each_shadow_including_descendant([&](auto& inclusive_descendant) {
+ JS::MarkedVector<JS::Value> arguments { vm.heap() };
+ arguments.append(&old_document);
+ arguments.append(this);
+
+ element.enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::adoptedCallback, move(arguments));
+ }
+
+ return IterationDecision::Continue;
+ });
+
+ // 3. For each inclusiveDescendant in nodeā€™s shadow-including inclusive descendants, in shadow-including tree order,
+ // run the adopting steps with inclusiveDescendant and oldDocument.
+ node.for_each_shadow_including_inclusive_descendant([&](auto& inclusive_descendant) {
inclusive_descendant.adopted_from(old_document);
return IterationDecision::Continue;
});
@@ -2049,6 +2096,36 @@ void Document::set_window(Badge<HTML::BrowsingContext>, HTML::Window& window)
m_window = &window;
}
+// https://html.spec.whatwg.org/multipage/custom-elements.html#look-up-a-custom-element-definition
+JS::GCPtr<HTML::CustomElementDefinition> Document::lookup_custom_element_definition(DeprecatedFlyString const& namespace_, DeprecatedFlyString const& local_name, Optional<String> const& is) const
+{
+ // 1. If namespace is not the HTML namespace, return null.
+ if (namespace_ != Namespace::HTML)
+ return nullptr;
+
+ // 2. If document's browsing context is null, return null.
+ if (!browsing_context())
+ return nullptr;
+
+ // 3. Let registry be document's relevant global object's CustomElementRegistry object.
+ auto registry = window().custom_elements().release_value_but_fixme_should_propagate_errors();
+
+ // 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 = String::from_utf8(local_name).release_value_but_fixme_should_propagate_errors();
+ 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.
+ // 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);
+}
+
CSS::StyleSheetList& Document::style_sheets()
{
if (!m_style_sheets)
@@ -2333,6 +2410,17 @@ bool Document::query_command_supported(DeprecatedString const& command) const
return false;
}
+void Document::increment_throw_on_dynamic_markup_insertion_counter(Badge<HTML::HTMLParser>)
+{
+ ++m_throw_on_dynamic_markup_insertion_counter;
+}
+
+void Document::decrement_throw_on_dynamic_markup_insertion_counter(Badge<HTML::HTMLParser>)
+{
+ VERIFY(m_throw_on_dynamic_markup_insertion_counter);
+ --m_throw_on_dynamic_markup_insertion_counter;
+}
+
// https://html.spec.whatwg.org/multipage/scripting.html#appropriate-template-contents-owner-document
JS::NonnullGCPtr<DOM::Document> Document::appropriate_template_contents_owner_document()
{
diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h
index 1346c8549c..7014e93a40 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.h
+++ b/Userland/Libraries/LibWeb/DOM/Document.h
@@ -70,6 +70,10 @@ struct DocumentUnloadTimingInfo {
double unload_event_end_time { 0 };
};
+struct ElementCreationOptions {
+ DeprecatedString is;
+};
+
class Document
: public ParentNode
, public NonElementParentNode<Document>
@@ -221,8 +225,8 @@ public:
JS::Value run_javascript(StringView source, StringView filename = "(unknown)"sv);
- WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element(DeprecatedFlyString const& local_name);
- WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element_ns(DeprecatedString const& namespace_, DeprecatedString const& qualified_name);
+ WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element(DeprecatedString const& local_name, Variant<DeprecatedString, ElementCreationOptions> const& options);
+ WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element_ns(DeprecatedString const& namespace_, DeprecatedString const& qualified_name, Variant<DeprecatedString, ElementCreationOptions> const& options);
JS::NonnullGCPtr<DocumentFragment> create_document_fragment();
JS::NonnullGCPtr<Text> create_text_node(DeprecatedString const& data);
JS::NonnullGCPtr<Comment> create_comment(DeprecatedString const& data);
@@ -399,6 +403,11 @@ public:
bool has_active_favicon() const { return m_active_favicon; }
void check_favicon_after_loading_link_resource();
+ JS::GCPtr<HTML::CustomElementDefinition> lookup_custom_element_definition(DeprecatedFlyString const& namespace_, DeprecatedFlyString const& local_name, Optional<String> const& is) const;
+
+ void increment_throw_on_dynamic_markup_insertion_counter(Badge<HTML::HTMLParser>);
+ void decrement_throw_on_dynamic_markup_insertion_counter(Badge<HTML::HTMLParser>);
+
// https://html.spec.whatwg.org/multipage/dom.html#is-initial-about:blank
bool is_initial_about_blank() const { return m_is_initial_about_blank; }
void set_is_initial_about_blank(bool b) { m_is_initial_about_blank = b; }
diff --git a/Userland/Libraries/LibWeb/DOM/Document.idl b/Userland/Libraries/LibWeb/DOM/Document.idl
index 144340746a..0615b780af 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.idl
+++ b/Userland/Libraries/LibWeb/DOM/Document.idl
@@ -74,8 +74,8 @@ interface Document : Node {
// FIXME: Should return an HTMLAllCollection
readonly attribute HTMLCollection all;
- Element createElement(DOMString tagName);
- Element createElementNS(DOMString? namespace, DOMString qualifiedName);
+ Element createElement(DOMString tagName, optional (DOMString or ElementCreationOptions) options = {});
+ Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional (DOMString or ElementCreationOptions) options = {});
DocumentFragment createDocumentFragment();
Text createTextNode(DOMString data);
Comment createComment(DOMString data);
@@ -114,5 +114,8 @@ interface Document : Node {
Selection? getSelection();
};
+dictionary ElementCreationOptions {
+ DOMString is;
+};
Document includes ParentNode;
Document includes GlobalEventHandlers;
diff --git a/Userland/Libraries/LibWeb/DOM/Element.cpp b/Userland/Libraries/LibWeb/DOM/Element.cpp
index c09f9d2f17..b7ddbbe169 100644
--- a/Userland/Libraries/LibWeb/DOM/Element.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Element.cpp
@@ -9,6 +9,7 @@
#include <AK/StringBuilder.h>
#include <LibWeb/Bindings/ElementPrototype.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
+#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/ResolvedCSSStyleDeclaration.h>
@@ -23,7 +24,10 @@
#include <LibWeb/Geometry/DOMRect.h>
#include <LibWeb/Geometry/DOMRectList.h>
#include <LibWeb/HTML/BrowsingContext.h>
+#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
#include <LibWeb/HTML/CustomElements/CustomElementName.h>
+#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
+#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLBodyElement.h>
#include <LibWeb/HTML/HTMLButtonElement.h>
@@ -36,6 +40,7 @@
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
+#include <LibWeb/HTML/Window.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Layout/BlockContainer.h>
@@ -50,6 +55,7 @@
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/PaintableBox.h>
+#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
@@ -83,6 +89,7 @@ void Element::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_inline_style.ptr());
visitor.visit(m_class_list.ptr());
visitor.visit(m_shadow_root.ptr());
+ visitor.visit(m_custom_element_definition.ptr());
for (auto& pseudo_element_layout_node : m_pseudo_element_nodes)
visitor.visit(pseudo_element_layout_node);
}
@@ -494,7 +501,15 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<ShadowRoot>> Element::attach_shadow(ShadowR
return WebIDL::NotSupportedError::create(realm(), DeprecatedString::formatted("Element '{}' cannot be a shadow host", local_name()));
}
- // FIXME: 3. If thisā€™s local name is a valid custom element name, or thisā€™s is value is not null, then: ...
+ // 3. If thisā€™s local name is a valid custom element name, or thisā€™s is value is not null, then:
+ if (HTML::is_valid_custom_element_name(local_name()) || m_is_value.has_value()) {
+ // 1. Let definition be the result of looking up a custom element definition given thisā€™s node document, its namespace, its local name, and its is value.
+ auto definition = document().lookup_custom_element_definition(namespace_(), local_name(), m_is_value);
+
+ // 2. If definition is not null and definitionā€™s disable shadow is true, then throw a "NotSupportedError" DOMException.
+ if (definition && definition->disable_shadow())
+ return WebIDL::NotSupportedError::create(realm(), "Cannot attach a shadow root to a custom element that has disabled shadow roots"sv);
+ }
// 4. If this is a shadow host, then throw an "NotSupportedError" DOMException.
if (is_shadow_host())
@@ -506,7 +521,9 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<ShadowRoot>> Element::attach_shadow(ShadowR
// 6. Set shadowā€™s delegates focus to init["delegatesFocus"].
shadow->set_delegates_focus(init.delegates_focus);
- // FIXME: 7. If thisā€™s custom element state is "precustomized" or "custom", then set shadowā€™s available to element internals to true.
+ // 7. If thisā€™s custom element state is "precustomized" or "custom", then set shadowā€™s available to element internals to true.
+ if (m_custom_element_state == CustomElementState::Precustomized || m_custom_element_state == CustomElementState::Custom)
+ shadow->set_available_to_element_internals(true);
// FIXME: 8. Set shadowā€™s slot assignment to init["slotAssignment"].
@@ -1424,4 +1441,226 @@ bool Element::include_in_accessibility_tree() const
return false;
}
+// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-an-element-on-the-appropriate-element-queue
+void Element::enqueue_an_element_on_the_appropriate_element_queue()
+{
+ // 1. Let reactionsStack be element's relevant agent's custom element reactions stack.
+ auto& relevant_agent = HTML::relevant_agent(*this);
+ auto* custom_data = verify_cast<Bindings::WebEngineCustomData>(relevant_agent.custom_data());
+ auto& reactions_stack = custom_data->custom_element_reactions_stack;
+
+ // 2. If reactionsStack is empty, then:
+ if (reactions_stack.element_queue_stack.is_empty()) {
+ // 1. Add element to reactionsStack's backup element queue.
+ reactions_stack.backup_element_queue.append(*this);
+
+ // 2. If reactionsStack's processing the backup element queue flag is set, then return.
+ if (reactions_stack.processing_the_backup_element_queue)
+ return;
+
+ // 3. Set reactionsStack's processing the backup element queue flag.
+ reactions_stack.processing_the_backup_element_queue = true;
+
+ // 4. Queue a microtask to perform the following steps:
+ // NOTE: `this` is protected by JS::SafeFunction
+ HTML::queue_a_microtask(&document(), [this]() {
+ auto& relevant_agent = HTML::relevant_agent(*this);
+ auto* custom_data = verify_cast<Bindings::WebEngineCustomData>(relevant_agent.custom_data());
+ auto& reactions_stack = custom_data->custom_element_reactions_stack;
+
+ // 1. Invoke custom element reactions in reactionsStack's backup element queue.
+ Bindings::invoke_custom_element_reactions(reactions_stack.backup_element_queue);
+
+ // 2. Unset reactionsStack's processing the backup element queue flag.
+ reactions_stack.processing_the_backup_element_queue = false;
+ });
+
+ return;
+ }
+
+ // 3. Otherwise, add element to element's relevant agent's current element queue.
+ custom_data->current_element_queue().append(*this);
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-a-custom-element-upgrade-reaction
+void Element::enqueue_a_custom_element_upgrade_reaction(HTML::CustomElementDefinition& custom_element_definition)
+{
+ // 1. Add a new upgrade reaction to element's custom element reaction queue, with custom element definition definition.
+ m_custom_element_reaction_queue.append(CustomElementUpgradeReaction { .custom_element_definition = custom_element_definition });
+
+ // 2. Enqueue an element on the appropriate element queue given element.
+ enqueue_an_element_on_the_appropriate_element_queue();
+}
+
+void Element::enqueue_a_custom_element_callback_reaction(FlyString const& callback_name, JS::MarkedVector<JS::Value> arguments)
+{
+ // 1. Let definition be element's custom element definition.
+ auto& definition = m_custom_element_definition;
+
+ // 2. Let callback be the value of the entry in definition's lifecycle callbacks with key callbackName.
+ auto callback_iterator = definition->lifecycle_callbacks().find(callback_name);
+
+ // 3. If callback is null, then return.
+ if (callback_iterator == definition->lifecycle_callbacks().end())
+ return;
+
+ if (callback_iterator->value.is_null())
+ return;
+
+ // 4. If callbackName is "attributeChangedCallback", then:
+ if (callback_name == HTML::CustomElementReactionNames::attributeChangedCallback) {
+ // 1. Let attributeName be the first element of args.
+ VERIFY(!arguments.is_empty());
+ auto& attribute_name_value = arguments.first();
+ VERIFY(attribute_name_value.is_string());
+ auto attribute_name = attribute_name_value.as_string().utf8_string().release_allocated_value_but_fixme_should_propagate_errors();
+
+ // 2. If definition's observed attributes does not contain attributeName, then return.
+ if (!definition->observed_attributes().contains_slow(attribute_name))
+ return;
+ }
+
+ // 5. Add a new callback reaction to element's custom element reaction queue, with callback function callback and arguments args.
+ m_custom_element_reaction_queue.append(CustomElementCallbackReaction { .callback = callback_iterator->value, .arguments = move(arguments) });
+
+ // 6. Enqueue an element on the appropriate element queue given element.
+ enqueue_an_element_on_the_appropriate_element_queue();
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-upgrade-an-element
+JS::ThrowCompletionOr<void> Element::upgrade_element(JS::NonnullGCPtr<HTML::CustomElementDefinition> custom_element_definition)
+{
+ auto& realm = this->realm();
+ auto& vm = this->vm();
+
+ // 1. If element's custom element state is not "undefined" or "uncustomized", then return.
+ if (m_custom_element_state != CustomElementState::Undefined && m_custom_element_state != CustomElementState::Uncustomized)
+ return {};
+
+ // 2. Set element's custom element definition to definition.
+ m_custom_element_definition = custom_element_definition;
+
+ // 3. Set element's custom element state to "failed".
+ m_custom_element_state = CustomElementState::Failed;
+
+ // 4. For each attribute in element's attribute list, in order, enqueue a custom element callback reaction with element, callback name "attributeChangedCallback",
+ // and an argument list containing attribute's local name, null, attribute's value, and attribute's namespace.
+ for (size_t attribute_index = 0; attribute_index < m_attributes->length(); ++attribute_index) {
+ auto const* attribute = m_attributes->item(attribute_index);
+ VERIFY(attribute);
+
+ JS::MarkedVector<JS::Value> arguments { vm.heap() };
+
+ arguments.append(JS::PrimitiveString::create(vm, attribute->local_name()));
+ arguments.append(JS::js_null());
+ arguments.append(JS::PrimitiveString::create(vm, attribute->value()));
+ arguments.append(JS::PrimitiveString::create(vm, attribute->namespace_uri()));
+
+ enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::attributeChangedCallback, move(arguments));
+ }
+
+ // 5. If element is connected, then enqueue a custom element callback reaction with element, callback name "connectedCallback", and an empty argument list.
+ if (is_connected()) {
+ JS::MarkedVector<JS::Value> empty_arguments { vm.heap() };
+ enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::connectedCallback, move(empty_arguments));
+ }
+
+ // 6. Add element to the end of definition's construction stack.
+ custom_element_definition->construction_stack().append(JS::make_handle(this));
+
+ // 7. Let C be definition's constructor.
+ auto& constructor = custom_element_definition->constructor();
+
+ // 8. Run the following substeps while catching any exceptions:
+ auto attempt_to_construct_custom_element = [&]() -> JS::ThrowCompletionOr<void> {
+ // 1. If definition's disable shadow is true and element's shadow root is non-null, then throw a "NotSupportedError" DOMException.
+ if (custom_element_definition->disable_shadow() && shadow_root())
+ return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Custom element definition disables shadow DOM and the custom element has a shadow root"sv));
+
+ // 2. Set element's custom element state to "precustomized".
+ m_custom_element_state = CustomElementState::Precustomized;
+
+ // 3. Let constructResult be the result of constructing C, with no arguments.
+ auto construct_result_optional = TRY(WebIDL::construct(constructor));
+ VERIFY(construct_result_optional.has_value());
+ auto construct_result = construct_result_optional.release_value();
+
+ // 4. If SameValue(constructResult, element) is false, then throw a TypeError.
+ if (!JS::same_value(construct_result, this))
+ return vm.throw_completion<JS::TypeError>("Constructing the custom element returned a different element from the custom element"sv);
+
+ return {};
+ };
+
+ auto maybe_exception = attempt_to_construct_custom_element();
+
+ // Then, perform the following substep, regardless of whether the above steps threw an exception or not:
+ // 1. Remove the last entry from the end of definition's construction stack.
+ (void)custom_element_definition->construction_stack().take_last();
+
+ // Finally, if the above steps threw an exception, then:
+ if (maybe_exception.is_throw_completion()) {
+ // 1. Set element's custom element definition to null.
+ m_custom_element_definition = nullptr;
+
+ // 2. Empty element's custom element reaction queue.
+ m_custom_element_reaction_queue.clear();
+
+ // 3. Rethrow the exception (thus terminating this algorithm).
+ return maybe_exception.release_error();
+ }
+
+ // FIXME: 9. If element is a form-associated custom element, then:
+ // 1. Reset the form owner of element. If element is associated with a form element, then enqueue a custom element callback reaction with element, callback name "formAssociatedCallback", and Ā« the associated form Ā».
+ // 2. If element is disabled, then enqueue a custom element callback reaction with element, callback name "formDisabledCallback" and Ā« true Ā».
+
+ // 10. Set element's custom element state to "custom".
+ m_custom_element_state = CustomElementState::Custom;
+
+ return {};
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-try-upgrade
+void Element::try_to_upgrade()
+{
+ // 1. Let definition be the result of looking up a custom element definition given element's node document, element's namespace, element's local name, and element's is value.
+ auto definition = document().lookup_custom_element_definition(namespace_(), local_name(), m_is_value);
+
+ // 2. If definition is not null, then enqueue a custom element upgrade reaction given element and definition.
+ if (definition)
+ enqueue_a_custom_element_upgrade_reaction(*definition);
+}
+
+// https://dom.spec.whatwg.org/#concept-element-defined
+bool Element::is_defined() const
+{
+ // An element whose custom element state is "uncustomized" or "custom" is said to be defined.
+ return m_custom_element_state == CustomElementState::Uncustomized || m_custom_element_state == CustomElementState::Custom;
+}
+
+// https://dom.spec.whatwg.org/#concept-element-custom
+bool Element::is_custom() const
+{
+ // An element whose custom element state is "custom" is said to be custom.
+ return m_custom_element_state == CustomElementState::Custom;
+}
+
+// https://html.spec.whatwg.org/multipage/dom.html#html-element-constructors
+void Element::setup_custom_element_from_constructor(HTML::CustomElementDefinition& custom_element_definition, Optional<String> const& is_value)
+{
+ // 7.6. Set element's custom element state to "custom".
+ m_custom_element_state = CustomElementState::Custom;
+
+ // 7.7. Set element's custom element definition to definition.
+ m_custom_element_definition = custom_element_definition;
+
+ // 7.8. Set element's is value to is value.
+ m_is_value = is_value;
+}
+
+void Element::set_prefix(DeprecatedFlyString const& value)
+{
+ m_qualified_name.set_prefix(value);
+}
+
}
diff --git a/Userland/Libraries/LibWeb/DOM/Element.h b/Userland/Libraries/LibWeb/DOM/Element.h
index db40cf0c21..3a7f4ece2e 100644
--- a/Userland/Libraries/LibWeb/DOM/Element.h
+++ b/Userland/Libraries/LibWeb/DOM/Element.h
@@ -41,6 +41,29 @@ struct ScrollIntoViewOptions : public HTML::ScrollOptions {
Bindings::ScrollLogicalPosition inline_ { Bindings::ScrollLogicalPosition::Nearest };
};
+// https://html.spec.whatwg.org/multipage/custom-elements.html#upgrade-reaction
+// An upgrade reaction, which will upgrade the custom element and contains a custom element definition; or
+struct CustomElementUpgradeReaction {
+ JS::Handle<HTML::CustomElementDefinition> custom_element_definition;
+};
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#callback-reaction
+// A callback reaction, which will call a lifecycle callback, and contains a callback function as well as a list of arguments.
+struct CustomElementCallbackReaction {
+ JS::Handle<WebIDL::CallbackType> callback;
+ JS::MarkedVector<JS::Value> arguments;
+};
+
+// https://dom.spec.whatwg.org/#concept-element-custom-element-state
+// An elementā€™s custom element state is one of "undefined", "failed", "uncustomized", "precustomized", or "custom".
+enum class CustomElementState {
+ Undefined,
+ Failed,
+ Uncustomized,
+ Precustomized,
+ Custom,
+};
+
class Element
: public ParentNode
, public ChildNode<Element>
@@ -60,6 +83,8 @@ public:
DeprecatedString const& tag_name() const { return html_uppercased_qualified_name(); }
DeprecatedFlyString const& prefix() const { return m_qualified_name.prefix(); }
+ void set_prefix(DeprecatedFlyString const& value);
+
DeprecatedFlyString const& namespace_() const { return m_qualified_name.namespace_(); }
// NOTE: This is for the JS bindings
@@ -259,6 +284,24 @@ public:
virtual bool include_in_accessibility_tree() const override;
+ void enqueue_a_custom_element_upgrade_reaction(HTML::CustomElementDefinition& custom_element_definition);
+ void enqueue_a_custom_element_callback_reaction(FlyString const& callback_name, JS::MarkedVector<JS::Value> arguments);
+
+ Vector<Variant<CustomElementUpgradeReaction, CustomElementCallbackReaction>>& custom_element_reaction_queue() { return m_custom_element_reaction_queue; }
+ Vector<Variant<CustomElementUpgradeReaction, CustomElementCallbackReaction>> const& custom_element_reaction_queue() const { return m_custom_element_reaction_queue; }
+
+ JS::ThrowCompletionOr<void> upgrade_element(JS::NonnullGCPtr<HTML::CustomElementDefinition> custom_element_definition);
+ void try_to_upgrade();
+
+ bool is_defined() const;
+ bool is_custom() const;
+
+ Optional<String> const& is_value() const { return m_is_value; }
+ void set_is_value(Optional<String> const& is) { m_is_value = is; }
+
+ void set_custom_element_state(CustomElementState value) { m_custom_element_state = value; }
+ void setup_custom_element_from_constructor(HTML::CustomElementDefinition& custom_element_definition, Optional<String> const& is_value);
+
protected:
Element(Document&, DOM::QualifiedName);
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
@@ -275,6 +318,8 @@ private:
WebIDL::ExceptionOr<JS::GCPtr<Node>> insert_adjacent(DeprecatedString const& where, JS::NonnullGCPtr<Node> node);
+ void enqueue_an_element_on_the_appropriate_element_queue();
+
QualifiedName m_qualified_name;
DeprecatedString m_html_uppercased_qualified_name;
@@ -289,6 +334,20 @@ private:
Vector<FlyString> m_classes;
Array<JS::GCPtr<Layout::Node>, to_underlying(CSS::Selector::PseudoElement::PseudoElementCount)> m_pseudo_element_nodes;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reaction-queue
+ // All elements have an associated custom element reaction queue, initially empty. Each item in the custom element reaction queue is of one of two types:
+ // NOTE: See the structs at the top of this header.
+ Vector<Variant<CustomElementUpgradeReaction, CustomElementCallbackReaction>> m_custom_element_reaction_queue;
+
+ // https://dom.spec.whatwg.org/#concept-element-custom-element-state
+ CustomElementState m_custom_element_state { CustomElementState::Undefined };
+
+ // https://dom.spec.whatwg.org/#concept-element-custom-element-definition
+ JS::GCPtr<HTML::CustomElementDefinition> m_custom_element_definition;
+
+ // https://dom.spec.whatwg.org/#concept-element-is-value
+ Optional<String> m_is_value;
};
template<>
diff --git a/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp b/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp
index ac55706a5b..757d0c9067 100644
--- a/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp
+++ b/Userland/Libraries/LibWeb/DOM/ElementFactory.cpp
@@ -1,12 +1,13 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
- * Copyright (c) 2020, Luke Wilde <lukew@serenityos.org>
+ * Copyright (c) 2020-2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/ElementFactory.h>
+#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
#include <LibWeb/HTML/CustomElements/CustomElementName.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLAreaElement.h>
@@ -78,6 +79,8 @@
#include <LibWeb/HTML/HTMLUListElement.h>
#include <LibWeb/HTML/HTMLUnknownElement.h>
#include <LibWeb/HTML/HTMLVideoElement.h>
+#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
+#include <LibWeb/Namespace.h>
#include <LibWeb/SVG/SVGCircleElement.h>
#include <LibWeb/SVG/SVGClipPathElement.h>
#include <LibWeb/SVG/SVGDefsElement.h>
@@ -92,34 +95,173 @@
#include <LibWeb/SVG/SVGSVGElement.h>
#include <LibWeb/SVG/SVGTextContentElement.h>
#include <LibWeb/SVG/TagNames.h>
+#include <LibWeb/WebIDL/AbstractOperations.h>
namespace Web::DOM {
-// https://dom.spec.whatwg.org/#concept-create-element
-WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element(Document& document, DeprecatedFlyString local_name, DeprecatedFlyString namespace_, DeprecatedFlyString prefix)
+ErrorOr<FixedArray<DeprecatedFlyString>> valid_local_names_for_given_html_element_interface(StringView html_element_interface_name)
{
- // 1. If prefix was not given, let prefix be null.
- // NOTE: This is already taken care of by `prefix` having a default value.
+ if (html_element_interface_name == "HTMLAnchorElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::a });
+ if (html_element_interface_name == "HTMLAreaElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::area });
+ if (html_element_interface_name == "HTMLAudioElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::audio });
+ if (html_element_interface_name == "HTMLBaseElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::base });
+ if (html_element_interface_name == "HTMLBodyElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::body });
+ if (html_element_interface_name == "HTMLBRElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::br });
+ if (html_element_interface_name == "HTMLButtonElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::button });
+ if (html_element_interface_name == "HTMLCanvasElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::canvas });
+ if (html_element_interface_name == "HTMLDataElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::data });
+ if (html_element_interface_name == "HTMLDataListElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::datalist });
+ if (html_element_interface_name == "HTMLDetailsElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::details });
+ if (html_element_interface_name == "HTMLDialogElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::dialog });
+ if (html_element_interface_name == "HTMLDirectoryElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::dir });
+ if (html_element_interface_name == "HTMLDivElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::div });
+ if (html_element_interface_name == "HTMLDListElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::dl });
+ if (html_element_interface_name == "HTMLEmbedElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::embed });
+ if (html_element_interface_name == "HTMLFieldsetElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::fieldset });
+ if (html_element_interface_name == "HTMLFontElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::font });
+ if (html_element_interface_name == "HTMLFormElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::form });
+ if (html_element_interface_name == "HTMLFrameElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::frame });
+ if (html_element_interface_name == "HTMLFrameSetElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::frameset });
+ if (html_element_interface_name == "HTMLHeadElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::head });
+ if (html_element_interface_name == "HTMLHeadingElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::h1, HTML::TagNames::h2, HTML::TagNames::h3, HTML::TagNames::h4, HTML::TagNames::h5, HTML::TagNames::h6 });
+ if (html_element_interface_name == "HTMLHRElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::hr });
+ if (html_element_interface_name == "HTMLHtmlElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::html });
+ if (html_element_interface_name == "HTMLIFrameElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::iframe });
+ if (html_element_interface_name == "HTMLImageElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::img });
+ if (html_element_interface_name == "HTMLInputElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::input });
+ if (html_element_interface_name == "HTMLLabelElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::label });
+ if (html_element_interface_name == "HTMLLIElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::li });
+ if (html_element_interface_name == "HTMLLinkElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::link });
+ if (html_element_interface_name == "HTMLMapElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::map });
+ if (html_element_interface_name == "HTMLMarqueeElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::marquee });
+ if (html_element_interface_name == "HTMLMenuElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::menu });
+ if (html_element_interface_name == "HTMLMeterElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::meter });
+ if (html_element_interface_name == "HTMLModElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::ins, HTML::TagNames::del });
+ if (html_element_interface_name == "HTMLObjectElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::object });
+ if (html_element_interface_name == "HTMLOutputElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::output });
+ if (html_element_interface_name == "HTMLParagraphElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::p });
+ if (html_element_interface_name == "HTMLParamElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::param });
+ if (html_element_interface_name == "HTMLPictureElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::picture });
+ if (html_element_interface_name == "HTMLPreElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::pre, HTML::TagNames::listing, HTML::TagNames::xmp });
+ if (html_element_interface_name == "HTMLProgressElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::progress });
+ if (html_element_interface_name == "HTMLQuoteElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::blockquote, HTML::TagNames::q });
+ if (html_element_interface_name == "HTMLScriptElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::script });
+ if (html_element_interface_name == "HTMLSelectElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::select });
+ if (html_element_interface_name == "HTMLSlotElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::slot });
+ if (html_element_interface_name == "HTMLSourceElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::source });
+ if (html_element_interface_name == "HTMLSpanElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::span });
+ if (html_element_interface_name == "HTMLStyleElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::style });
+ if (html_element_interface_name == "HTMLTableCaptionElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::caption });
+ if (html_element_interface_name == "HTMLTableCellElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::td, HTML::TagNames::th });
+ if (html_element_interface_name == "HTMLTableColElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::colgroup, HTML::TagNames::col });
+ if (html_element_interface_name == "HTMLTableElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::table });
+ if (html_element_interface_name == "HTMLTableSectionElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::tbody, HTML::TagNames::thead, HTML::TagNames::tfoot });
+ if (html_element_interface_name == "HTMLTemplateElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::template_ });
+ if (html_element_interface_name == "HTMLTextAreaElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::textarea });
+ if (html_element_interface_name == "HTMLTimeElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::time });
+ if (html_element_interface_name == "HTMLTitleElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::title });
+ if (html_element_interface_name == "HTMLTrackElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::track });
+ if (html_element_interface_name == "HTMLUListElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::ul });
+ if (html_element_interface_name == "HTMLVideoElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::video });
+ if (html_element_interface_name == "HTMLElement"sv)
+ return FixedArray<DeprecatedFlyString>::create({ HTML::TagNames::article, HTML::TagNames::section, HTML::TagNames::nav, HTML::TagNames::aside, HTML::TagNames::hgroup, HTML::TagNames::header, HTML::TagNames::footer, HTML::TagNames::address, HTML::TagNames::dt, HTML::TagNames::dd, HTML::TagNames::figure, HTML::TagNames::figcaption, HTML::TagNames::main, HTML::TagNames::em, HTML::TagNames::strong, HTML::TagNames::small, HTML::TagNames::s, HTML::TagNames::cite, HTML::TagNames::dfn, HTML::TagNames::abbr, HTML::TagNames::ruby, HTML::TagNames::rt, HTML::TagNames::rp, HTML::TagNames::code, HTML::TagNames::var, HTML::TagNames::samp, HTML::TagNames::kbd, HTML::TagNames::sub, HTML::TagNames::sup, HTML::TagNames::i, HTML::TagNames::b, HTML::TagNames::u, HTML::TagNames::mark, HTML::TagNames::bdi, HTML::TagNames::bdo, HTML::TagNames::wbr, HTML::TagNames::summary, HTML::TagNames::noscript, HTML::TagNames::acronym, HTML::TagNames::basefont, HTML::TagNames::big, HTML::TagNames::center, HTML::TagNames::nobr, HTML::TagNames::noembed, HTML::TagNames::noframes, HTML::TagNames::plaintext, HTML::TagNames::rb, HTML::TagNames::rtc, HTML::TagNames::strike, HTML::TagNames::tt });
+ return FixedArray<DeprecatedFlyString>::create({});
+}
- // FIXME: 2. If is was not given, let is be null.
- // FIXME: 3. Let result be null.
- // FIXME: 4. Let definition be the result of looking up a custom element definition given document, namespace, localName, and is.
- // FIXME: 5. If definition is non-null, and definitionā€™s name is not equal to its local name (i.e., definition represents a customized built-in element), then: ...
- // FIXME: 6. Otherwise, if definition is non-null, then: ...
+// https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom%3Aelement-interface
+bool is_unknown_html_element(DeprecatedFlyString const& tag_name)
+{
+ // NOTE: This is intentionally case-sensitive.
- // 7. Otherwise:
- // 1. Let interface be the element interface for localName and namespace.
- // 2. Set result to a new element that implements interface, with no attributes, namespace set to namespace, namespace prefix set to prefix,
- // local name set to localName, custom element state set to "uncustomized", custom element definition set to null, is value set to is,
- // and node document set to document.
- // FIXME: 3. If namespace is the HTML namespace, and either localName is a valid custom element name or is is non-null,
- // then set resultā€™s custom element state to "undefined".
- // 8. Return result.
+ // 1. If name is applet, bgsound, blink, isindex, keygen, multicol, nextid, or spacer, then return HTMLUnknownElement.
+ if (tag_name.is_one_of(HTML::TagNames::applet, HTML::TagNames::bgsound, HTML::TagNames::blink, HTML::TagNames::isindex, HTML::TagNames::keygen, HTML::TagNames::multicol, HTML::TagNames::nextid, HTML::TagNames::spacer))
+ return true;
- auto& realm = document.realm();
- auto lowercase_tag_name = local_name.to_lowercase();
+ // 2. If name is acronym, basefont, big, center, nobr, noembed, noframes, plaintext, rb, rtc, strike, or tt, then return HTMLElement.
+ // 3. If name is listing or xmp, then return HTMLPreElement.
+ // 4. Otherwise, if this specification defines an interface appropriate for the element type corresponding to the local name name, then return that interface.
+ // 5. If other applicable specifications define an appropriate interface for name, then return the interface they define.
+#define __ENUMERATE_HTML_TAG(name) \
+ if (tag_name == HTML::TagNames::name) \
+ return false;
+ ENUMERATE_HTML_TAGS
+#undef __ENUMERATE_HTML_TAG
+
+ // 6. If name is a valid custom element name, then return HTMLElement.
+ if (HTML::is_valid_custom_element_name(tag_name))
+ return false;
+
+ // 7. Return HTMLUnknownElement.
+ return true;
+}
+
+// https://html.spec.whatwg.org/#elements-in-the-dom:element-interface
+static WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_html_element(JS::Realm& realm, Document& document, QualifiedName qualified_name)
+{
+ auto lowercase_tag_name = qualified_name.local_name().to_lowercase();
- auto qualified_name = QualifiedName { local_name, prefix, namespace_ };
if (lowercase_tag_name == HTML::TagNames::a)
return MUST_OR_THROW_OOM(realm.heap().allocate<HTML::HTMLAnchorElement>(realm, document, move(qualified_name)));
if (lowercase_tag_name == HTML::TagNames::area)
@@ -264,39 +406,214 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element(Document& document
// Obsolete
HTML::TagNames::acronym, HTML::TagNames::basefont, HTML::TagNames::big, HTML::TagNames::center, HTML::TagNames::nobr, HTML::TagNames::noembed, HTML::TagNames::noframes, HTML::TagNames::plaintext, HTML::TagNames::rb, HTML::TagNames::rtc, HTML::TagNames::strike, HTML::TagNames::tt))
return MUST_OR_THROW_OOM(realm.heap().allocate<HTML::HTMLElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::svg)
+ if (HTML::is_valid_custom_element_name(qualified_name.local_name()))
+ return MUST_OR_THROW_OOM(realm.heap().allocate<HTML::HTMLElement>(realm, document, move(qualified_name)));
+
+ return MUST_OR_THROW_OOM(realm.heap().allocate<HTML::HTMLUnknownElement>(realm, document, move(qualified_name)));
+}
+
+static WebIDL::ExceptionOr<JS::GCPtr<SVG::SVGElement>> create_svg_element(JS::Realm& realm, Document& document, QualifiedName qualified_name)
+{
+ auto const& local_name = qualified_name.local_name();
+
+ if (local_name == SVG::TagNames::svg)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGSVGElement>(realm, document, move(qualified_name)));
// FIXME: Support SVG's mixedCase tag names properly.
- if (lowercase_tag_name.equals_ignoring_ascii_case(SVG::TagNames::clipPath))
+ if (local_name.equals_ignoring_ascii_case(SVG::TagNames::clipPath))
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGClipPathElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::circle)
+ if (local_name == SVG::TagNames::circle)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGCircleElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name.equals_ignoring_ascii_case(SVG::TagNames::defs))
+ if (local_name.equals_ignoring_ascii_case(SVG::TagNames::defs))
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGDefsElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::ellipse)
+ if (local_name == SVG::TagNames::ellipse)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGEllipseElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name.equals_ignoring_ascii_case(SVG::TagNames::foreignObject))
+ if (local_name.equals_ignoring_ascii_case(SVG::TagNames::foreignObject))
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGForeignObjectElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::line)
+ if (local_name == SVG::TagNames::line)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGLineElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::path)
+ if (local_name == SVG::TagNames::path)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGPathElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::polygon)
+ if (local_name == SVG::TagNames::polygon)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGPolygonElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::polyline)
+ if (local_name == SVG::TagNames::polyline)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGPolylineElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::rect)
+ if (local_name == SVG::TagNames::rect)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGRectElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::g)
+ if (local_name == SVG::TagNames::g)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGGElement>(realm, document, move(qualified_name)));
- if (lowercase_tag_name == SVG::TagNames::text)
+ if (local_name == SVG::TagNames::text)
return MUST_OR_THROW_OOM(realm.heap().allocate<SVG::SVGTextContentElement>(realm, document, move(qualified_name)));
- // If name is a valid custom element name, then return HTMLElement.
- if (HTML::is_valid_custom_element_name(local_name)) {
- return MUST_OR_THROW_OOM(realm.heap().allocate<HTML::HTMLElement>(realm, document, move(qualified_name)));
+ return nullptr;
+}
+
+// https://dom.spec.whatwg.org/#concept-create-element
+WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element(Document& document, DeprecatedFlyString local_name, DeprecatedFlyString namespace_, DeprecatedFlyString prefix, Optional<String> is_value, bool synchronous_custom_elements_flag)
+{
+ auto& realm = document.realm();
+
+ // 1. If prefix was not given, let prefix be null.
+ // NOTE: This is already taken care of by `prefix` having a default value.
+
+ // 2. If is was not given, let is be null.
+ // NOTE: This is already taken care of by `is` having a default value.
+
+ // 3. Let result be null.
+ // NOTE: We collapse this into just returning an element where necessary.
+
+ // 4. Let definition be the result of looking up a custom element definition given document, namespace, localName, and is.
+ auto definition = document.lookup_custom_element_definition(namespace_, local_name, is_value);
+
+ // 5. If definition is non-null, and definitionā€™s name is not equal to its local name (i.e., definition represents a customized built-in element), then:
+ if (definition && definition->name() != definition->local_name()) {
+ // 1. Let interface be the element interface for localName and the HTML namespace.
+ // 2. Set result to a new element that implements interface, with no attributes, namespace set to the HTML namespace,
+ // namespace prefix set to prefix, local name set to localName, custom element state set to "undefined", custom element definition set to null,
+ // is value set to is, and node document set to document.
+ auto element = TRY(create_html_element(realm, document, QualifiedName { local_name, prefix, Namespace::HTML }));
+
+ // 3. If the synchronous custom elements flag is set, then run this step while catching any exceptions:
+ if (synchronous_custom_elements_flag) {
+ // 1. Upgrade element using definition.
+ auto upgrade_result = element->upgrade_element(*definition);
+
+ // If this step threw an exception, then:
+ if (upgrade_result.is_throw_completion()) {
+ // 1. Report the exception.
+ HTML::report_exception(upgrade_result, realm);
+
+ // 2. Set resultā€™s custom element state to "failed".
+ element->set_custom_element_state(CustomElementState::Failed);
+ }
+ }
+
+ // 4. Otherwise, enqueue a custom element upgrade reaction given result and definition.
+ else {
+ element->enqueue_a_custom_element_upgrade_reaction(*definition);
+ }
+
+ return element;
}
- return MUST_OR_THROW_OOM(realm.heap().allocate<HTML::HTMLUnknownElement>(realm, document, move(qualified_name)));
+
+ // 6. Otherwise, if definition is non-null, then:
+ if (definition) {
+ // 1. If the synchronous custom elements flag is set, then run these steps while catching any exceptions:
+ if (synchronous_custom_elements_flag) {
+ auto synchronously_upgrade_custom_element = [&]() -> JS::ThrowCompletionOr<JS::NonnullGCPtr<HTML::HTMLElement>> {
+ auto& vm = document.vm();
+
+ // 1. Let C be definitionā€™s constructor.
+ auto& constructor = definition->constructor();
+
+ // 2. Set result to the result of constructing C, with no arguments.
+ auto result = TRY(WebIDL::construct(constructor));
+
+ // FIXME: 3. Assert: resultā€™s custom element state and custom element definition are initialized.
+ // FIXME: 4. Assert: resultā€™s namespace is the HTML namespace.
+ // Spec Note: IDL enforces that result is an HTMLElement object, which all use the HTML namespace.
+ // IDL does not currently convert the object for us, so we will have to do it here.
+
+ if (!result.has_value() || !result->is_object() || !is<HTML::HTMLElement>(result->as_object()))
+ return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObjectOfType, "HTMLElement"sv);
+
+ JS::NonnullGCPtr<HTML::HTMLElement> element = verify_cast<HTML::HTMLElement>(result->as_object());
+
+ // 5. If resultā€™s attribute list is not empty, then throw a "NotSupportedError" DOMException.
+ if (element->has_attributes())
+ return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Synchronously created custom element cannot have attributes"sv));
+
+ // 6. If result has children, then throw a "NotSupportedError" DOMException.
+ if (element->has_children())
+ return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Synchronously created custom element cannot have children"sv));
+
+ // 7. If resultā€™s parent is not null, then throw a "NotSupportedError" DOMException.
+ if (element->parent())
+ return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Synchronously created custom element cannot have a parent"sv));
+
+ // 8. If resultā€™s node document is not document, then throw a "NotSupportedError" DOMException.
+ if (&element->document() != &document)
+ return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Synchronously created custom element must be in the same document that element creation was invoked in"sv));
+
+ // 9. If resultā€™s local name is not equal to localName, then throw a "NotSupportedError" DOMException.
+ if (element->local_name() != local_name)
+ return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Synchronously created custom element must have the same local name that element creation was invoked with"sv));
+
+ // 10. Set resultā€™s namespace prefix to prefix.
+ element->set_prefix(prefix);
+
+ // 11. Set resultā€™s is value to null.
+ element->set_is_value(Optional<String> {});
+ return element;
+ };
+
+ auto result = synchronously_upgrade_custom_element();
+
+ // If any of these steps threw an exception, then:
+ if (result.is_throw_completion()) {
+ // 1. Report the exception.
+ HTML::report_exception(result, realm);
+
+ // 2. Set result to a new element that implements the HTMLUnknownElement interface, with no attributes, namespace set to the HTML namespace, namespace prefix set to prefix,
+ // local name set to localName, custom element state set to "failed", custom element definition set to null, is value set to null, and node document set to document.
+ JS::NonnullGCPtr<Element> element = realm.heap().allocate<HTML::HTMLUnknownElement>(realm, document, QualifiedName { local_name, prefix, Namespace::HTML }).release_allocated_value_but_fixme_should_propagate_errors();
+ element->set_custom_element_state(CustomElementState::Failed);
+ return element;
+ }
+
+ return result.release_value();
+ }
+
+ // 2. Otherwise:
+ // 1. Set result to a new element that implements the HTMLElement interface, with no attributes, namespace set to the HTML namespace, namespace prefix set to prefix,
+ // local name set to localName, custom element state set to "undefined", custom element definition set to null, is value set to null, and node document set to document.
+ JS::NonnullGCPtr<Element> element = realm.heap().allocate<HTML::HTMLElement>(realm, document, QualifiedName { local_name, prefix, Namespace::HTML }).release_allocated_value_but_fixme_should_propagate_errors();
+ element->set_custom_element_state(CustomElementState::Undefined);
+
+ // 2. Enqueue a custom element upgrade reaction given result and definition.
+ element->enqueue_a_custom_element_upgrade_reaction(*definition);
+ return element;
+ }
+
+ // 7. Otherwise:
+ // 1. Let interface be the element interface for localName and namespace.
+ // 2. Set result to a new element that implements interface, with no attributes, namespace set to namespace, namespace prefix set to prefix,
+ // local name set to localName, custom element state set to "uncustomized", custom element definition set to null, is value set to is,
+ // and node document set to document.
+
+ auto qualified_name = QualifiedName { local_name, prefix, namespace_ };
+
+ if (namespace_ == Namespace::HTML) {
+ auto element = TRY(create_html_element(realm, document, move(qualified_name)));
+ element->set_is_value(move(is_value));
+ element->set_custom_element_state(CustomElementState::Uncustomized);
+
+ // 3. If namespace is the HTML namespace, and either localName is a valid custom element name or is is non-null,
+ // then set resultā€™s custom element state to "undefined".
+ if (HTML::is_valid_custom_element_name(local_name) || is_value.has_value())
+ element->set_custom_element_state(CustomElementState::Undefined);
+
+ return element;
+ }
+
+ if (namespace_ == Namespace::SVG) {
+ auto element = TRY(create_svg_element(realm, document, qualified_name));
+ if (element) {
+ element->set_is_value(move(is_value));
+ element->set_custom_element_state(CustomElementState::Uncustomized);
+ return JS::NonnullGCPtr<Element> { *element };
+ }
+ }
+
+ // 8. Return result.
+ // NOTE: See step 3.
+
+ // https://dom.spec.whatwg.org/#concept-element-interface
+ // The element interface for any name and namespace is Element, unless stated otherwise.
+ dbgln("Potential FIXME: Creating unknown generic element '{}' in namespace '{}'", local_name, namespace_);
+ JS::NonnullGCPtr<Element> element = realm.heap().allocate<DOM::Element>(realm, document, move(qualified_name)).release_allocated_value_but_fixme_should_propagate_errors();
+ element->set_is_value(move(is_value));
+ element->set_custom_element_state(CustomElementState::Uncustomized);
+ return element;
}
}
diff --git a/Userland/Libraries/LibWeb/DOM/ElementFactory.h b/Userland/Libraries/LibWeb/DOM/ElementFactory.h
index cccdc9c886..e436e7746d 100644
--- a/Userland/Libraries/LibWeb/DOM/ElementFactory.h
+++ b/Userland/Libraries/LibWeb/DOM/ElementFactory.h
@@ -6,10 +6,15 @@
#pragma once
+#include <AK/FixedArray.h>
#include <LibWeb/DOM/Element.h>
namespace Web::DOM {
-WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element(Document&, DeprecatedFlyString local_name, DeprecatedFlyString namespace_, DeprecatedFlyString prefix = {});
+ErrorOr<FixedArray<DeprecatedFlyString>> valid_local_names_for_given_html_element_interface(StringView html_element_interface_name);
+bool is_unknown_html_element(DeprecatedFlyString const& tag_name);
+
+// FIXME: The spec doesn't say what the default value of synchronous_custom_elements_flag should be.
+WebIDL::ExceptionOr<JS::NonnullGCPtr<Element>> create_element(Document&, DeprecatedFlyString local_name, DeprecatedFlyString namespace_, DeprecatedFlyString prefix = {}, Optional<String> is = Optional<String> {}, bool synchronous_custom_elements_flag = false);
}
diff --git a/Userland/Libraries/LibWeb/DOM/Node.cpp b/Userland/Libraries/LibWeb/DOM/Node.cpp
index 4d7d29543f..0c43633a0d 100644
--- a/Userland/Libraries/LibWeb/DOM/Node.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Node.cpp
@@ -30,6 +30,7 @@
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/StaticNodeList.h>
#include <LibWeb/HTML/BrowsingContextContainer.h>
+#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLStyleElement.h>
#include <LibWeb/HTML/Origin.h>
@@ -438,18 +439,29 @@ void Node::insert_before(JS::NonnullGCPtr<Node> node, JS::GCPtr<Node> child, boo
// FIXME: 5. If parentā€™s root is a shadow root, and parent is a slot whose assigned nodes is the empty list, then run signal a slot change for parent.
// FIXME: 6. Run assign slottables for a tree with nodeā€™s root.
- // FIXME: This should be shadow-including.
// 7. For each shadow-including inclusive descendant inclusiveDescendant of node, in shadow-including tree order:
- node_to_insert->for_each_in_inclusive_subtree([&](Node& inclusive_descendant) {
+ node_to_insert->for_each_shadow_including_inclusive_descendant([&](Node& inclusive_descendant) {
// 1. Run the insertion steps with inclusiveDescendant.
inclusive_descendant.inserted();
// 2. If inclusiveDescendant is connected, then:
- if (inclusive_descendant.is_connected()) {
- // FIXME: 1. If inclusiveDescendant is custom, then enqueue a custom element callback reaction with inclusiveDescendant, callback name "connectedCallback", and an empty argument list.
+ // NOTE: This is not specified here in the spec, but these steps can only be performed on an element.
+ if (inclusive_descendant.is_connected() && is<DOM::Element>(inclusive_descendant)) {
+ auto& element = static_cast<DOM::Element&>(inclusive_descendant);
+
+ // 1. If inclusiveDescendant is custom, then enqueue a custom element callback reaction with inclusiveDescendant,
+ // callback name "connectedCallback", and an empty argument list.
+ if (element.is_custom()) {
+ JS::MarkedVector<JS::Value> empty_arguments { vm().heap() };
+ element.enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::connectedCallback, move(empty_arguments));
+ }
- // FIXME: 2. Otherwise, try to upgrade inclusiveDescendant.
- // NOTE: If this successfully upgrades inclusiveDescendant, its connectedCallback will be enqueued automatically during the upgrade an element algorithm.
+ // 2. Otherwise, try to upgrade inclusiveDescendant.
+ // NOTE: If this successfully upgrades inclusiveDescendant, its connectedCallback will be enqueued automatically during
+ // the upgrade an element algorithm.
+ else {
+ element.try_to_upgrade();
+ }
}
return IterationDecision::Continue;
@@ -579,20 +591,37 @@ void Node::remove(bool suppress_observers)
// 15. Run the removing steps with node and parent.
removed_from(parent);
- // FIXME: 16. Let isParentConnected be parentā€™s connected. (Currently unused so not included)
+ // 16. Let isParentConnected be parentā€™s connected.
+ bool is_parent_connected = parent->is_connected();
- // FIXME: 17. If node is custom and isParentConnected is true, then enqueue a custom element callback reaction with node,
- // callback name "disconnectedCallback", and an empty argument list.
- // NOTE: It is intentional for now that custom elements do not get parent passed. This might change in the future if there is a need.
+ // 17. If node is custom and isParentConnected is true, then enqueue a custom element callback reaction with node,
+ // callback name "disconnectedCallback", and an empty argument list.
+ // Spec Note: It is intentional for now that custom elements do not get parent passed.
+ // This might change in the future if there is a need.
+ if (is<DOM::Element>(*this)) {
+ auto& element = static_cast<DOM::Element&>(*this);
+
+ if (element.is_custom() && is_parent_connected) {
+ JS::MarkedVector<JS::Value> empty_arguments { vm().heap() };
+ element.enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::disconnectedCallback, move(empty_arguments));
+ }
+ }
- // FIXME: This should be shadow-including.
// 18. For each shadow-including descendant descendant of node, in shadow-including tree order, then:
- for_each_in_subtree([&](Node& descendant) {
+ for_each_shadow_including_descendant([&](Node& descendant) {
// 1. Run the removing steps with descendant
descendant.removed_from(nullptr);
- // FIXME: 2. If descendant is custom and isParentConnected is true, then enqueue a custom element callback reaction with descendant,
- // callback name "disconnectedCallback", and an empty argument list.
+ // 2. If descendant is custom and isParentConnected is true, then enqueue a custom element callback reaction with descendant,
+ // callback name "disconnectedCallback", and an empty argument list.
+ if (is<DOM::Element>(descendant)) {
+ auto& element = static_cast<DOM::Element&>(descendant);
+
+ if (element.is_custom() && is_parent_connected) {
+ JS::MarkedVector<JS::Value> empty_arguments { vm().heap() };
+ element.enqueue_a_custom_element_callback_reaction(HTML::CustomElementReactionNames::disconnectedCallback, move(empty_arguments));
+ }
+ }
return IterationDecision::Continue;
});
@@ -727,7 +756,7 @@ JS::NonnullGCPtr<Node> Node::clone_node(Document* document, bool clone_children)
if (is<Element>(this)) {
// 1. Let copy be the result of creating an element, given document, nodeā€™s local name, nodeā€™s namespace, nodeā€™s namespace prefix, and nodeā€™s is value, with the synchronous custom elements flag unset.
auto& element = *verify_cast<Element>(this);
- auto element_copy = DOM::create_element(*document, element.local_name(), element.namespace_() /* FIXME: nodeā€™s namespace prefix, and nodeā€™s is value, with the synchronous custom elements flag unset */).release_value_but_fixme_should_propagate_errors();
+ auto element_copy = DOM::create_element(*document, element.local_name(), element.namespace_(), element.prefix(), element.is_value(), false).release_value_but_fixme_should_propagate_errors();
// 2. For each attribute in nodeā€™s attribute list:
element.for_each_attribute([&](auto& name, auto& value) {
diff --git a/Userland/Libraries/LibWeb/DOM/Node.h b/Userland/Libraries/LibWeb/DOM/Node.h
index 82dd81b346..6832d4c372 100644
--- a/Userland/Libraries/LibWeb/DOM/Node.h
+++ b/Userland/Libraries/LibWeb/DOM/Node.h
@@ -241,6 +241,10 @@ public:
void queue_mutation_record(FlyString const& type, DeprecatedString attribute_name, DeprecatedString attribute_namespace, DeprecatedString old_value, JS::NonnullGCPtr<NodeList> added_nodes, JS::NonnullGCPtr<NodeList> removed_nodes, Node* previous_sibling, Node* next_sibling) const;
+ // https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-descendant
+ template<typename Callback>
+ IterationDecision for_each_shadow_including_inclusive_descendant(Callback);
+
// https://dom.spec.whatwg.org/#concept-shadow-including-descendant
template<typename Callback>
IterationDecision for_each_shadow_including_descendant(Callback);
diff --git a/Userland/Libraries/LibWeb/DOM/QualifiedName.cpp b/Userland/Libraries/LibWeb/DOM/QualifiedName.cpp
index 46ee475ea8..28fb86d74a 100644
--- a/Userland/Libraries/LibWeb/DOM/QualifiedName.cpp
+++ b/Userland/Libraries/LibWeb/DOM/QualifiedName.cpp
@@ -70,4 +70,9 @@ void QualifiedName::Impl::make_internal_string()
as_string = DeprecatedString::formatted("{}:{}", prefix, local_name);
}
+void QualifiedName::set_prefix(DeprecatedFlyString const& value)
+{
+ m_impl->prefix = value;
+}
+
}
diff --git a/Userland/Libraries/LibWeb/DOM/QualifiedName.h b/Userland/Libraries/LibWeb/DOM/QualifiedName.h
index 14d69d60bc..4c90cbdd5b 100644
--- a/Userland/Libraries/LibWeb/DOM/QualifiedName.h
+++ b/Userland/Libraries/LibWeb/DOM/QualifiedName.h
@@ -32,6 +32,8 @@ public:
DeprecatedString as_string;
};
+ void set_prefix(DeprecatedFlyString const& value);
+
private:
NonnullRefPtr<Impl> m_impl;
};
diff --git a/Userland/Libraries/LibWeb/DOM/ShadowRoot.h b/Userland/Libraries/LibWeb/DOM/ShadowRoot.h
index 46a5b10338..db9a24ba61 100644
--- a/Userland/Libraries/LibWeb/DOM/ShadowRoot.h
+++ b/Userland/Libraries/LibWeb/DOM/ShadowRoot.h
@@ -47,18 +47,34 @@ template<>
inline bool Node::fast_is<ShadowRoot>() const { return is_shadow_root(); }
template<typename Callback>
-inline IterationDecision Node::for_each_shadow_including_descendant(Callback callback)
+inline IterationDecision Node::for_each_shadow_including_inclusive_descendant(Callback callback)
{
if (callback(*this) == IterationDecision::Break)
return IterationDecision::Break;
for (auto* child = first_child(); child; child = child->next_sibling()) {
if (child->is_element()) {
if (JS::GCPtr<ShadowRoot> shadow_root = static_cast<Element*>(child)->shadow_root_internal()) {
- if (shadow_root->for_each_shadow_including_descendant(callback) == IterationDecision::Break)
+ if (shadow_root->for_each_shadow_including_inclusive_descendant(callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ }
+ }
+ if (child->for_each_shadow_including_inclusive_descendant(callback) == IterationDecision::Break)
+ return IterationDecision::Break;
+ }
+ return IterationDecision::Continue;
+}
+
+template<typename Callback>
+inline IterationDecision Node::for_each_shadow_including_descendant(Callback callback)
+{
+ for (auto* child = first_child(); child; child = child->next_sibling()) {
+ if (child->is_element()) {
+ if (JS::GCPtr<ShadowRoot> shadow_root = static_cast<Element*>(child)->shadow_root()) {
+ if (shadow_root->for_each_shadow_including_inclusive_descendant(callback) == IterationDecision::Break)
return IterationDecision::Break;
}
}
- if (child->for_each_shadow_including_descendant(callback) == IterationDecision::Break)
+ if (child->for_each_shadow_including_inclusive_descendant(callback) == IterationDecision::Break)
return IterationDecision::Break;
}
return IterationDecision::Continue;
diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h
index 8f7b9d0af0..b20e081070 100644
--- a/Userland/Libraries/LibWeb/Forward.h
+++ b/Userland/Libraries/LibWeb/Forward.h
@@ -247,6 +247,8 @@ class ClassicScript;
class CloseEvent;
struct CrossOriginOpenerPolicy;
struct CrossOriginOpenerPolicyEnforcementResult;
+class CustomElementDefinition;
+class CustomElementRegistry;
class DOMParser;
class DOMStringMap;
struct Environment;
diff --git a/Userland/Libraries/LibWeb/HTML/AttributeNames.h b/Userland/Libraries/LibWeb/HTML/AttributeNames.h
index 9d602b6c0e..08838deee0 100644
--- a/Userland/Libraries/LibWeb/HTML/AttributeNames.h
+++ b/Userland/Libraries/LibWeb/HTML/AttributeNames.h
@@ -83,6 +83,7 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(imagesrcset) \
__ENUMERATE_HTML_ATTRIBUTE(inert) \
__ENUMERATE_HTML_ATTRIBUTE(integrity) \
+ __ENUMERATE_HTML_ATTRIBUTE(is) \
__ENUMERATE_HTML_ATTRIBUTE(ismap) \
__ENUMERATE_HTML_ATTRIBUTE(itemscope) \
__ENUMERATE_HTML_ATTRIBUTE(label) \
diff --git a/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp b/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp
index 7649bcf37d..53b3aa1f4a 100644
--- a/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp
+++ b/Userland/Libraries/LibWeb/HTML/BrowsingContext.cpp
@@ -6,6 +6,7 @@
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/DOM/Document.h>
+#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/Range.h>
@@ -27,6 +28,7 @@
#include <LibWeb/Layout/BreakNode.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/Viewport.h>
+#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/URL/URL.h>
@@ -191,9 +193,9 @@ JS::NonnullGCPtr<BrowsingContext> BrowsingContext::create_a_new_browsing_context
document->set_is_initial_about_blank(true);
// 18. Ensure that document has a single child html node, which itself has two empty child nodes: a head element, and a body element.
- auto html_node = document->create_element(HTML::TagNames::html).release_value();
- MUST(html_node->append_child(document->create_element(HTML::TagNames::head).release_value()));
- MUST(html_node->append_child(document->create_element(HTML::TagNames::body).release_value()));
+ auto html_node = DOM::create_element(document, HTML::TagNames::html, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
+ MUST(html_node->append_child(DOM::create_element(document, HTML::TagNames::head, Namespace::HTML).release_value_but_fixme_should_propagate_errors()));
+ MUST(html_node->append_child(DOM::create_element(document, HTML::TagNames::body, Namespace::HTML).release_value_but_fixme_should_propagate_errors()));
MUST(document->append_child(html_node));
// 19. Set the active document of browsingContext to document.
diff --git a/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementDefinition.h b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementDefinition.h
new file mode 100644
index 0000000000..ddfd9e7a98
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementDefinition.h
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/String.h>
+#include <LibWeb/HTML/HTMLElement.h>
+#include <LibWeb/WebIDL/CallbackType.h>
+
+namespace Web::HTML {
+
+struct AlreadyConstructedCustomElementMarker {
+};
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-definition
+class CustomElementDefinition : public JS::Cell {
+ JS_CELL(CustomElementDefinition, JS::Cell);
+
+ using LifecycleCallbacksStorage = OrderedHashMap<FlyString, JS::Handle<WebIDL::CallbackType>>;
+ using ConstructionStackStorage = Vector<Variant<JS::Handle<DOM::Element>, AlreadyConstructedCustomElementMarker>>;
+
+ static JS::NonnullGCPtr<CustomElementDefinition> create(JS::Realm& realm, String const& name, String const& local_name, WebIDL::CallbackType& constructor, Vector<String>&& observed_attributes, LifecycleCallbacksStorage&& lifecycle_callbacks, bool form_associated, bool disable_internals, bool disable_shadow)
+ {
+ return realm.heap().allocate<CustomElementDefinition>(realm, name, local_name, constructor, move(observed_attributes), move(lifecycle_callbacks), form_associated, disable_internals, disable_shadow).release_allocated_value_but_fixme_should_propagate_errors();
+ }
+
+ ~CustomElementDefinition() = default;
+
+ String const& name() const { return m_name; }
+ String const& local_name() const { return m_local_name; }
+
+ WebIDL::CallbackType& constructor() { return *m_constructor; }
+ WebIDL::CallbackType const& constructor() const { return *m_constructor; }
+
+ Vector<String> const& observed_attributes() const { return m_observed_attributes; }
+
+ LifecycleCallbacksStorage const& lifecycle_callbacks() const { return m_lifecycle_callbacks; }
+
+ ConstructionStackStorage& construction_stack() { return m_construction_stack; }
+ ConstructionStackStorage const& construction_stack() const { return m_construction_stack; }
+
+ bool form_associated() const { return m_form_associated; }
+ bool disable_internals() const { return m_disable_internals; }
+ bool disable_shadow() const { return m_disable_shadow; }
+
+private:
+ CustomElementDefinition(String const& name, String const& local_name, WebIDL::CallbackType& constructor, Vector<String>&& observed_attributes, LifecycleCallbacksStorage&& lifecycle_callbacks, bool form_associated, bool disable_internals, bool disable_shadow)
+ : m_name(name)
+ , m_local_name(local_name)
+ , m_constructor(JS::make_handle(constructor))
+ , m_observed_attributes(move(observed_attributes))
+ , m_lifecycle_callbacks(move(lifecycle_callbacks))
+ , m_form_associated(form_associated)
+ , m_disable_internals(disable_internals)
+ , m_disable_shadow(disable_shadow)
+ {
+ }
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-name
+ // A name
+ // A valid custom element name
+ String m_name;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-local-name
+ // A local name
+ // A local name
+ String m_local_name;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-constructor
+ // A Web IDL CustomElementConstructor callback function type value wrapping the custom element constructor
+ JS::Handle<WebIDL::CallbackType> m_constructor;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-observed-attributes
+ // A list of observed attributes
+ // A sequence<DOMString>
+ Vector<String> m_observed_attributes;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-lifecycle-callbacks
+ // A collection of lifecycle callbacks
+ // A map, whose keys are the strings "connectedCallback", "disconnectedCallback", "adoptedCallback", "attributeChangedCallback",
+ // "formAssociatedCallback", "formDisabledCallback", "formResetCallback", and "formStateRestoreCallback".
+ // The corresponding values are either a Web IDL Function callback function type value, or null.
+ // By default the value of each entry is null.
+ LifecycleCallbacksStorage m_lifecycle_callbacks;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-construction-stack
+ // A construction stack
+ // A list, initially empty, that is manipulated by the upgrade an element algorithm and the HTML element constructors.
+ // Each entry in the list will be either an element or an already constructed marker.
+ ConstructionStackStorage m_construction_stack;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-form-associated
+ // A form-associated boolean
+ // If this is true, user agent treats elements associated to this custom element definition as form-associated custom elements.
+ bool m_form_associated { false };
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-disable-internals
+ // A disable internals boolean
+ // Controls attachInternals().
+ bool m_disable_internals { false };
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-disable-shadow
+ // A disable shadow boolean
+ // Controls attachShadow().
+ bool m_disable_shadow { false };
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.cpp b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.cpp
new file mode 100644
index 0000000000..cab0d2b681
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
+
+namespace Web::HTML::CustomElementReactionNames {
+
+#define __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(name) FlyString name;
+ENUMERATE_CUSTOM_ELEMENT_REACTION_NAMES
+#undef __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME
+
+ErrorOr<void> initialize_strings()
+{
+ static bool s_initialized = false;
+ VERIFY(!s_initialized);
+
+#define __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(name) \
+ name = TRY(#name##_fly_string);
+ ENUMERATE_CUSTOM_ELEMENT_REACTION_NAMES
+#undef __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME
+
+ s_initialized = true;
+ return {};
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.h b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.h
new file mode 100644
index 0000000000..ed54dfcc33
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementReactionNames.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/FlyString.h>
+
+namespace Web::HTML::CustomElementReactionNames {
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-lifecycle-callbacks
+#define ENUMERATE_CUSTOM_ELEMENT_REACTION_NAMES \
+ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(connectedCallback) \
+ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(disconnectedCallback) \
+ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(adoptedCallback) \
+ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(attributeChangedCallback) \
+ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formAssociatedCallback) \
+ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formDisabledCallback) \
+ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formResetCallback) \
+ __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formStateRestoreCallback)
+
+#define __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(name) extern FlyString name;
+ENUMERATE_CUSTOM_ELEMENT_REACTION_NAMES
+#undef __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME
+
+ErrorOr<void> initialize_strings();
+
+}
diff --git a/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp
new file mode 100644
index 0000000000..6f23225b9b
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.cpp
@@ -0,0 +1,411 @@
+/*
+ * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibJS/Runtime/FunctionObject.h>
+#include <LibJS/Runtime/IteratorOperations.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 {
+
+CustomElementRegistry::CustomElementRegistry(JS::Realm& realm)
+ : Bindings::PlatformObject(realm)
+{
+}
+
+CustomElementRegistry::~CustomElementRegistry() = default;
+
+JS::ThrowCompletionOr<void> CustomElementRegistry::initialize(JS::Realm& realm)
+{
+ MUST_OR_THROW_OOM(Base::initialize(realm));
+ set_prototype(&Bindings::ensure_web_prototype<Bindings::CustomElementRegistryPrototype>(realm, "CustomElementRegistry"));
+
+ return {};
+}
+
+// https://webidl.spec.whatwg.org/#es-callback-function
+static JS::ThrowCompletionOr<JS::NonnullGCPtr<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, TRY_OR_THROW_OOM(vm, value.to_string_without_side_effects()));
+
+ // 2. Return the IDL callback function type value that represents a reference to the same object that V represents, with the incumbent settings object as the callback context.
+ return vm.heap().allocate_without_realm<WebIDL::CallbackType>(value.as_object(), HTML::incumbent_settings_object());
+}
+
+// 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 Type(V) is not Object, throw a TypeError.
+ if (!value.is_object())
+ return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, TRY_OR_THROW_OOM(vm, value.to_string_without_side_effects()));
+
+ // 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, TRY_OR_THROW_OOM(vm, value.to_string_without_side_effects()));
+
+ // 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).
+ auto iterator = TRY(JS::get_iterator(vm, value, JS::IteratorHint::Sync, 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)
+ return sequence_of_strings;
+
+ // 3. Let nextItem be ? IteratorValue(next).
+ auto next_item = TRY(JS::iterator_value(vm, *next));
+
+ // 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, TRY_OR_THROW_OOM(vm, JS::Value(constructor->callback).to_string_without_side_effects()));
+
+ // 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, DeprecatedString::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.
+ auto existing_definition_with_name_iterator = m_custom_element_definitions.find_if([&name](JS::Handle<CustomElementDefinition> 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, DeprecatedString::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.
+ auto existing_definition_with_constructor_iterator = m_custom_element_definitions.find_if([&constructor](JS::Handle<CustomElementDefinition> 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"sv));
+
+ // 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.
+ auto& extends = options.extends;
+
+ // 7. If extends is not null, then:
+ 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, DeprecatedString::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.
+ if (DOM::is_unknown_html_element(extends.value().to_deprecated_string()))
+ return JS::throw_completion(WebIDL::NotSupportedError::create(realm, DeprecatedString::formatted("'{}' is an unknown HTML element"sv, extends.value())));
+
+ // 3. Set localName to extends.
+ local_name = extends.value();
+ }
+
+ // 8. If this CustomElementRegistry's element definition is running flag is set, then throw a "NotSupportedError" DOMException.
+ if (m_element_definition_is_running)
+ return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Cannot recursively define custom elements"sv));
+
+ // 9. Set this CustomElementRegistry's element definition is running flag.
+ 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;
+
+ // 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:
+ 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 Type(prototype) is not Object, then throw a TypeError exception.
+ if (!prototype_value.is_object())
+ return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, TRY_OR_THROW_OOM(vm, prototype_value.to_string_without_side_effects()));
+
+ 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.
+ 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:
+ 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.
+ if (!callback_value.is_undefined()) {
+ auto callback = TRY(convert_value_to_callback_function(vm, callback_value));
+ lifecycle_callbacks.set(callback_name, JS::make_handle(callback));
+ }
+ }
+
+ // 5. If the value of the entry in lifecycleCallbacks with key "attributeChangedCallback" is not null, then:
+ auto attribute_changed_callback_iterator = lifecycle_callbacks.find(CustomElementReactionNames::attributeChangedCallback);
+ VERIFY(attribute_changed_callback_iterator != lifecycle_callbacks.end());
+ if (!attribute_changed_callback_iterator->value.is_null()) {
+ // 1. Let observedAttributesIterable be ? Get(constructor, "observedAttributes").
+ auto observed_attributes_iterable = TRY(constructor->callback->get(JS::PropertyKey { "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(JS::PropertyKey { "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. Set disableInternals to true if disabledFeatures contains "internals".
+ disable_internals = disabled_features.contains_slow(TRY_OR_THROW_OOM(vm, "internals"_string));
+
+ // 10. Set disableShadow to true if disabledFeatures contains "shadow".
+ disable_shadow = disabled_features.contains_slow(TRY_OR_THROW_OOM(vm, "shadow"_string));
+
+ // 11. Let formAssociatedValue be ? Get( constructor, "formAssociated").
+ auto form_associated_value = TRY(constructor->callback->get(JS::PropertyKey { "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.
+ form_associated = form_associated_value.to_boolean();
+
+ // 13. If formAssociated is true, for each of "formAssociatedCallback", "formResetCallback", "formDisabledCallback", and "formStateRestoreCallback" callbackName:
+ 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.
+ if (!callback_value.is_undefined())
+ lifecycle_callbacks.set(callback_name, JS::make_handle(TRY(convert_value_to_callback_function(vm, callback_value))));
+ }
+ }
+
+ return {};
+ };
+
+ 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.
+ 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.
+ 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. Add definition to this CustomElementRegistry.
+ m_custom_element_definitions.append(JS::make_handle(*definition));
+
+ // 17. Let document be this CustomElementRegistry'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.
+ // Additionally, if extends is non-null, only include elements whose is value is equal to name.
+ Vector<JS::Handle<DOM::Element>> upgrade_candidates;
+
+ document.for_each_shadow_including_descendant([&](DOM::Node& inclusive_descendant) {
+ if (!is<DOM::Element>(inclusive_descendant))
+ return IterationDecision::Continue;
+
+ auto& inclusive_descendant_element = static_cast<DOM::Element&>(inclusive_descendant);
+
+ if (inclusive_descendant_element.namespace_() == Namespace::HTML && inclusive_descendant_element.local_name() == local_name.to_deprecated_string() && (!extends.has_value() || inclusive_descendant_element.is_value() == name))
+ upgrade_candidates.append(JS::make_handle(inclusive_descendant_element));
+
+ return IterationDecision::Continue;
+ });
+
+ // 19. For each element element in upgrade candidates, 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:
+ 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.cell();
+
+ // 2. Resolve promise with constructor.
+ promise->fulfill(constructor->callback);
+
+ // 3. Delete the entry with key name from this CustomElementRegistry's when-defined promise map.
+ m_when_defined_promise_map.remove(name);
+ }
+
+ return {};
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-get
+Variant<JS::Handle<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.
+ auto existing_definition_iterator = m_custom_element_definitions.find_if([&name](JS::Handle<CustomElementDefinition> const& definition) {
+ return definition->name() == name;
+ });
+
+ if (!existing_definition_iterator.is_end())
+ return JS::make_handle(existing_definition_iterator->cell()->constructor());
+
+ // 2. Otherwise, return undefined.
+ return JS::js_undefined();
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-whendefined
+WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> CustomElementRegistry::when_defined(String const& name)
+{
+ auto& realm = this->realm();
+
+ // 1. If name is not a valid custom element name, then return a new promise rejected with a "SyntaxError" DOMException.
+ if (!is_valid_custom_element_name(name)) {
+ auto promise = JS::Promise::create(realm);
+ promise->reject(WebIDL::SyntaxError::create(realm, DeprecatedString::formatted("'{}' is not a valid custom element name"sv, name)));
+ return promise;
+ }
+
+ // 2. If this CustomElementRegistry contains an entry with name name, then return a new promise resolved with that entry's constructor.
+ auto existing_definition_iterator = m_custom_element_definitions.find_if([&name](JS::Handle<CustomElementDefinition> const& definition) {
+ return definition->name() == name;
+ });
+
+ if (existing_definition_iterator != m_custom_element_definitions.end()) {
+ auto promise = JS::Promise::create(realm);
+ promise->fulfill(existing_definition_iterator->cell()->constructor().callback);
+ return promise;
+ }
+
+ // 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.
+ JS::GCPtr<JS::Promise> 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.cell();
+ } else {
+ promise = JS::Promise::create(realm);
+ m_when_defined_promise_map.set(name, JS::make_handle(promise));
+ }
+
+ // 5. Return promise.
+ VERIFY(promise);
+ return JS::NonnullGCPtr { *promise };
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-upgrade
+void CustomElementRegistry::upgrade(JS::NonnullGCPtr<DOM::Node> root) const
+{
+ // 1. Let candidates be a list of all of root's shadow-including inclusive descendant elements, in shadow-including tree order.
+ Vector<JS::Handle<DOM::Element>> candidates;
+
+ root->for_each_shadow_including_inclusive_descendant([&](DOM::Node& inclusive_descendant) {
+ if (!is<DOM::Element>(inclusive_descendant))
+ return IterationDecision::Continue;
+
+ auto& inclusive_descendant_element = static_cast<DOM::Element&>(inclusive_descendant);
+ candidates.append(JS::make_handle(inclusive_descendant_element));
+
+ return IterationDecision::Continue;
+ });
+
+ // 2. For each candidate of candidates, try to upgrade candidate.
+ for (auto& candidate : candidates)
+ candidate->try_to_upgrade();
+}
+
+JS::GCPtr<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([&](JS::Handle<CustomElementDefinition> const& definition) {
+ return definition->name() == name && definition->local_name() == local_name;
+ });
+
+ return definition_iterator.is_end() ? nullptr : definition_iterator->ptr();
+}
+
+JS::GCPtr<CustomElementDefinition> CustomElementRegistry::get_definition_from_new_target(JS::FunctionObject const& new_target) const
+{
+ auto definition_iterator = m_custom_element_definitions.find_if([&](JS::Handle<CustomElementDefinition> const& definition) {
+ return definition->constructor().callback.ptr() == &new_target;
+ });
+
+ return definition_iterator.is_end() ? nullptr : definition_iterator->ptr();
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.h b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.h
new file mode 100644
index 0000000000..9fb9c1d621
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/RefCounted.h>
+#include <LibWeb/Bindings/PlatformObject.h>
+#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
+
+namespace Web::HTML {
+
+struct ElementDefinitionOptions {
+ Optional<String> extends;
+};
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#customelementregistry
+class CustomElementRegistry : public Bindings::PlatformObject {
+ WEB_PLATFORM_OBJECT(CustomElementRegistry, Bindings::PlatformObject);
+
+public:
+ virtual ~CustomElementRegistry() override;
+
+ JS::ThrowCompletionOr<void> define(String const& name, WebIDL::CallbackType* constructor, ElementDefinitionOptions options);
+ Variant<JS::Handle<WebIDL::CallbackType>, JS::Value> get(String const& name) const;
+ WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> when_defined(String const& name);
+ void upgrade(JS::NonnullGCPtr<DOM::Node> root) const;
+
+ JS::GCPtr<CustomElementDefinition> get_definition_with_name_and_local_name(String const& name, String const& local_name) const;
+ JS::GCPtr<CustomElementDefinition> get_definition_from_new_target(JS::FunctionObject const& new_target) const;
+
+private:
+ CustomElementRegistry(JS::Realm&);
+
+ virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
+
+ // Every CustomElementRegistry has a set of custom element definitions, initially empty. In general, algorithms in this specification look up elements in the registry by any of name, local name, or constructor.
+ Vector<JS::Handle<CustomElementDefinition>> m_custom_element_definitions;
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#element-definition-is-running
+ // Every CustomElementRegistry also has an element definition is running flag which is used to prevent reentrant invocations of element definition. It is initially unset.
+ bool m_element_definition_is_running { false };
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#when-defined-promise-map
+ // Every CustomElementRegistry also has a when-defined promise map, mapping valid custom element names to promises. It is used to implement the whenDefined() method.
+ OrderedHashMap<String, JS::Handle<JS::Promise>> m_when_defined_promise_map;
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.idl b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.idl
new file mode 100644
index 0000000000..d4e8ade4ce
--- /dev/null
+++ b/Userland/Libraries/LibWeb/HTML/CustomElements/CustomElementRegistry.idl
@@ -0,0 +1,15 @@
+#import <DOM/Node.idl>
+
+[Exposed=Window, UseNewAKString]
+interface CustomElementRegistry {
+ [CEReactions] undefined define(DOMString name, CustomElementConstructor constructor, optional ElementDefinitionOptions options = {});
+ (CustomElementConstructor or undefined) get(DOMString name);
+ Promise<CustomElementConstructor> whenDefined(DOMString name);
+ [CEReactions] undefined upgrade(Node root);
+};
+
+callback CustomElementConstructor = HTMLElement ();
+
+dictionary ElementDefinitionOptions {
+ DOMString extends;
+};
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
index 00ff9cb2bb..2da21fb220 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp
@@ -8,6 +8,7 @@
#include <LibWeb/CSS/StyleValues/IdentifierStyleValue.h>
#include <LibWeb/DOM/Document.h>
+#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
@@ -21,6 +22,7 @@
#include <LibWeb/Layout/ButtonBox.h>
#include <LibWeb/Layout/CheckBox.h>
#include <LibWeb/Layout/RadioButton.h>
+#include <LibWeb/Namespace.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
@@ -408,7 +410,7 @@ void HTMLInputElement::create_shadow_tree_if_needed()
auto initial_value = m_value;
if (initial_value.is_null())
initial_value = DeprecatedString::empty();
- auto element = document().create_element(HTML::TagNames::div).release_value();
+ auto element = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(element->set_attribute(HTML::AttributeNames::style, "white-space: pre; padding-top: 1px; padding-bottom: 1px; padding-left: 2px; padding-right: 2px; height: 1lh;"));
m_text_node = heap().allocate<DOM::Text>(realm(), document(), initial_value).release_allocated_value_but_fixme_should_propagate_errors();
m_text_node->set_always_editable(m_type != TypeAttributeState::FileUpload);
diff --git a/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp b/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp
index ae94d43a08..fccbd1f256 100644
--- a/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp
+++ b/Userland/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp
@@ -19,6 +19,7 @@
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/ProcessingInstruction.h>
#include <LibWeb/DOM/Text.h>
+#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/HTMLFormElement.h>
@@ -629,18 +630,36 @@ JS::NonnullGCPtr<DOM::Element> HTMLParser::create_element_for(HTMLToken const& t
// 4. Let local name be the tag name of the token.
auto local_name = token.tag_name();
- // FIXME: 5. Let is be the value of the "is" attribute in the given token, if such an attribute exists, or null otherwise.
- // FIXME: 6. Let definition be the result of looking up a custom element definition given document, given namespace, local name, and is.
- // FIXME: 7. If definition is non-null and the parser was not created as part of the HTML fragment parsing algorithm, then let will execute script be true. Otherwise, let it be false.
- // FIXME: 8. If will execute script is true, then:
- // FIXME: 1. Increment document's throw-on-dynamic-markup-insertion counter.
- // FIXME: 2. If the JavaScript execution context stack is empty, then perform a microtask checkpoint.
- // FIXME: 3. Push a new element queue onto document's relevant agent's custom element reactions stack.
+ // 5. Let is be the value of the "is" attribute in the given token, if such an attribute exists, or null otherwise.
+ auto is_value_deprecated_string = token.attribute(AttributeNames::is);
+ Optional<String> is_value;
+ if (!is_value_deprecated_string.is_null())
+ is_value = String::from_utf8(is_value_deprecated_string).release_value_but_fixme_should_propagate_errors();
+
+ // 6. Let definition be the result of looking up a custom element definition given document, given namespace, local name, and is.
+ auto definition = document->lookup_custom_element_definition(namespace_, local_name, is_value);
+
+ // 7. If definition is non-null and the parser was not created as part of the HTML fragment parsing algorithm, then let will execute script be true. Otherwise, let it be false.
+ bool will_execute_script = definition && !m_parsing_fragment;
+
+ // 8. If will execute script is true, then:
+ if (will_execute_script) {
+ // 1. Increment document's throw-on-dynamic-markup-insertion counter.
+ document->increment_throw_on_dynamic_markup_insertion_counter({});
+
+ // 2. If the JavaScript execution context stack is empty, then perform a microtask checkpoint.
+ auto& vm = main_thread_event_loop().vm();
+ if (vm.execution_context_stack().is_empty())
+ perform_a_microtask_checkpoint();
+
+ // 3. Push a new element queue onto document's relevant agent's custom element reactions stack.
+ auto& custom_data = verify_cast<Bindings::WebEngineCustomData>(*vm.custom_data());
+ custom_data.custom_element_reactions_stack.element_queue_stack.append({});
+ }
// 9. Let element be the result of creating an element given document, localName, given namespace, null, and is.
- // FIXME: If will execute script is true, set the synchronous custom elements flag; otherwise, leave it unset.
- // FIXME: Pass in `null` and `is`.
- auto element = create_element(*document, local_name, namespace_).release_value_but_fixme_should_propagate_errors();
+ // If will execute script is true, set the synchronous custom elements flag; otherwise, leave it unset.
+ auto element = create_element(*document, local_name, namespace_, {}, is_value, will_execute_script).release_value_but_fixme_should_propagate_errors();
// 10. Append each attribute in the given token to element.
// FIXME: This isn't the exact `append` the spec is talking about.
@@ -649,10 +668,19 @@ JS::NonnullGCPtr<DOM::Element> HTMLParser::create_element_for(HTMLToken const& t
return IterationDecision::Continue;
});
- // FIXME: 11. If will execute script is true, then:
- // FIXME: 1. Let queue be the result of popping from document's relevant agent's custom element reactions stack. (This will be the same element queue as was pushed above.)
- // FIXME: 2. Invoke custom element reactions in queue.
- // FIXME: 3. Decrement document's throw-on-dynamic-markup-insertion counter.
+ // 11. If will execute script is true, then:
+ if (will_execute_script) {
+ // 1. Let queue be the result of popping from document's relevant agent's custom element reactions stack. (This will be the same element queue as was pushed above.)
+ auto& vm = main_thread_event_loop().vm();
+ auto& custom_data = verify_cast<Bindings::WebEngineCustomData>(*vm.custom_data());
+ auto queue = custom_data.custom_element_reactions_stack.element_queue_stack.take_last();
+
+ // 2. Invoke custom element reactions in queue.
+ Bindings::invoke_custom_element_reactions(queue);
+
+ // 3. Decrement document's throw-on-dynamic-markup-insertion counter.
+ document->decrement_throw_on_dynamic_markup_insertion_counter({});
+ }
// FIXME: 12. If element has an xmlns attribute in the XMLNS namespace whose value is not exactly the same as the element's namespace, that is a parse error.
// Similarly, if element has an xmlns:xlink attribute in the XMLNS namespace whose value is not the XLink Namespace, that is a parse error.
@@ -694,14 +722,22 @@ JS::NonnullGCPtr<DOM::Element> HTMLParser::insert_foreign_element(HTMLToken cons
// NOTE: If it's not possible to insert the element at the adjusted insertion location, the element is simply dropped.
if (!pre_insertion_validity.is_exception()) {
+ // 1. If the parser was not created as part of the HTML fragment parsing algorithm, then push a new element queue onto element's relevant agent's custom element reactions stack.
if (!m_parsing_fragment) {
- // FIXME: push a new element queue onto element's relevant agent's custom element reactions stack.
+ auto& vm = main_thread_event_loop().vm();
+ auto& custom_data = verify_cast<Bindings::WebEngineCustomData>(*vm.custom_data());
+ custom_data.custom_element_reactions_stack.element_queue_stack.append({});
}
+ // 2. Insert element at the adjusted insertion location.
adjusted_insertion_location.parent->insert_before(*element, adjusted_insertion_location.insert_before_sibling);
+ // 3. If the parser was not created as part of the HTML fragment parsing algorithm, then pop the element queue from element's relevant agent's custom element reactions stack, and invoke custom element reactions in that queue.
if (!m_parsing_fragment) {
- // FIXME: pop the element queue from element's relevant agent's custom element reactions stack, and invoke custom element reactions in that queue.
+ auto& vm = main_thread_event_loop().vm();
+ auto& custom_data = verify_cast<Bindings::WebEngineCustomData>(*vm.custom_data());
+ auto queue = custom_data.custom_element_reactions_stack.element_queue_stack.take_last();
+ Bindings::invoke_custom_element_reactions(queue);
}
}
@@ -2269,6 +2305,12 @@ void HTMLParser::handle_text(HTMLToken& token)
// Non-standard: Make sure the <script> element has up-to-date text content before preparing the script.
flush_character_insertions();
+ // If the active speculative HTML parser is null and the JavaScript execution context stack is empty, then perform a microtask checkpoint.
+ // FIXME: If the active speculative HTML parser is null
+ auto& vm = main_thread_event_loop().vm();
+ if (vm.execution_context_stack().is_empty())
+ perform_a_microtask_checkpoint();
+
// Let script be the current node (which will be a script element).
JS::NonnullGCPtr<HTMLScriptElement> script = verify_cast<HTMLScriptElement>(current_node());
@@ -3634,9 +3676,14 @@ DeprecatedString HTMLParser::serialize_html_fragment(DOM::Node const& node)
builder.append('<');
builder.append(tag_name);
- // FIXME: 3. If current node's is value is not null, and the element does not have an is attribute in its attribute list,
- // then append the string " is="", followed by current node's is value escaped as described below in attribute mode,
- // followed by a U+0022 QUOTATION MARK character (").
+ // 3. If current node's is value is not null, and the element does not have an is attribute in its attribute list,
+ // then append the string " is="", followed by current node's is value escaped as described below in attribute mode,
+ // followed by a U+0022 QUOTATION MARK character (").
+ if (element.is_value().has_value() && !element.has_attribute(AttributeNames::is)) {
+ builder.append(" is=\""sv);
+ builder.append(escape_string(element.is_value().value(), AttributeMode::Yes));
+ builder.append('"');
+ }
// 4. For each attribute that the element has, append a U+0020 SPACE character, the attribute's serialized name as described below, a U+003D EQUALS SIGN character (=),
// a U+0022 QUOTATION MARK character ("), the attribute's value, escaped as described below in attribute mode, and a second U+0022 QUOTATION MARK character (").
diff --git a/Userland/Libraries/LibWeb/HTML/Parser/HTMLToken.h b/Userland/Libraries/LibWeb/HTML/Parser/HTMLToken.h
index c49d98ab65..bfc32c8c4a 100644
--- a/Userland/Libraries/LibWeb/HTML/Parser/HTMLToken.h
+++ b/Userland/Libraries/LibWeb/HTML/Parser/HTMLToken.h
@@ -247,7 +247,7 @@ public:
}
}
- StringView attribute(DeprecatedFlyString const& attribute_name)
+ StringView attribute(DeprecatedFlyString const& attribute_name) const
{
VERIFY(is_start_tag() || is_end_tag());
diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/Environments.cpp b/Userland/Libraries/LibWeb/HTML/Scripting/Environments.cpp
index 605ffb8f27..a5a3d41bf6 100644
--- a/Userland/Libraries/LibWeb/HTML/Scripting/Environments.cpp
+++ b/Userland/Libraries/LibWeb/HTML/Scripting/Environments.cpp
@@ -435,6 +435,13 @@ JS::Object& entry_global_object()
return entry_realm().global_object();
}
+JS::VM& relevant_agent(JS::Object const& object)
+{
+ // The relevant agent for a platform object platformObject is platformObject's relevant Realm's agent.
+ // Spec Note: This pointer is not yet defined in the JavaScript specification; see tc39/ecma262#1357.
+ return relevant_realm(object).vm();
+}
+
// https://html.spec.whatwg.org/multipage/webappapis.html#secure-context
bool is_secure_context(Environment const& environment)
{
diff --git a/Userland/Libraries/LibWeb/HTML/Scripting/Environments.h b/Userland/Libraries/LibWeb/HTML/Scripting/Environments.h
index 3bccf5ef08..d2a7bec305 100644
--- a/Userland/Libraries/LibWeb/HTML/Scripting/Environments.h
+++ b/Userland/Libraries/LibWeb/HTML/Scripting/Environments.h
@@ -148,6 +148,7 @@ JS::Object& relevant_global_object(JS::Object const&);
JS::Realm& entry_realm();
EnvironmentSettingsObject& entry_settings_object();
JS::Object& entry_global_object();
+JS::VM& relevant_agent(JS::Object const&);
[[nodiscard]] bool is_secure_context(Environment const&);
[[nodiscard]] bool is_non_secure_context(Environment const&);
diff --git a/Userland/Libraries/LibWeb/HTML/TagNames.h b/Userland/Libraries/LibWeb/HTML/TagNames.h
index eb0efb7604..82726734b0 100644
--- a/Userland/Libraries/LibWeb/HTML/TagNames.h
+++ b/Userland/Libraries/LibWeb/HTML/TagNames.h
@@ -78,6 +78,7 @@ namespace Web::HTML::TagNames {
__ENUMERATE_HTML_TAG(img) \
__ENUMERATE_HTML_TAG(input) \
__ENUMERATE_HTML_TAG(ins) \
+ __ENUMERATE_HTML_TAG(isindex) \
__ENUMERATE_HTML_TAG(kbd) \
__ENUMERATE_HTML_TAG(keygen) \
__ENUMERATE_HTML_TAG(label) \
@@ -94,7 +95,9 @@ namespace Web::HTML::TagNames {
__ENUMERATE_HTML_TAG(menuitem) \
__ENUMERATE_HTML_TAG(meta) \
__ENUMERATE_HTML_TAG(meter) \
+ __ENUMERATE_HTML_TAG(multicol) \
__ENUMERATE_HTML_TAG(nav) \
+ __ENUMERATE_HTML_TAG(nextid) \
__ENUMERATE_HTML_TAG(nobr) \
__ENUMERATE_HTML_TAG(noembed) \
__ENUMERATE_HTML_TAG(noframes) \
@@ -126,6 +129,7 @@ namespace Web::HTML::TagNames {
__ENUMERATE_HTML_TAG(small) \
__ENUMERATE_HTML_TAG(source) \
__ENUMERATE_HTML_TAG(span) \
+ __ENUMERATE_HTML_TAG(spacer) \
__ENUMERATE_HTML_TAG(strike) \
__ENUMERATE_HTML_TAG(strong) \
__ENUMERATE_HTML_TAG(style) \
diff --git a/Userland/Libraries/LibWeb/HTML/Window.cpp b/Userland/Libraries/LibWeb/HTML/Window.cpp
index fe7a8041a9..e3f821056e 100644
--- a/Userland/Libraries/LibWeb/HTML/Window.cpp
+++ b/Userland/Libraries/LibWeb/HTML/Window.cpp
@@ -32,6 +32,7 @@
#include <LibWeb/DOM/EventDispatcher.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/HTML/BrowsingContext.h>
+#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/Focus.h>
@@ -104,6 +105,7 @@ void Window::visit_edges(JS::Cell::Visitor& visitor)
visitor.visit(m_location);
visitor.visit(m_crypto);
visitor.visit(m_navigator);
+ visitor.visit(m_custom_element_registry);
for (auto& plugin_object : m_pdf_viewer_plugin_objects)
visitor.visit(plugin_object);
for (auto& mime_type_object : m_pdf_viewer_mime_type_objects)
@@ -1325,6 +1327,17 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Crypto::Crypto>> Window::crypto()
return JS::NonnullGCPtr { *m_crypto };
}
+// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-window-customelements
+WebIDL::ExceptionOr<JS::NonnullGCPtr<CustomElementRegistry>> Window::custom_elements()
+{
+ auto& realm = this->realm();
+
+ // The customElements attribute of the Window interface must return the CustomElementRegistry object for that Window object.
+ if (!m_custom_element_registry)
+ m_custom_element_registry = MUST_OR_THROW_OOM(heap().allocate<CustomElementRegistry>(realm, realm));
+ return JS::NonnullGCPtr { *m_custom_element_registry };
+}
+
// https://html.spec.whatwg.org/multipage/window-object.html#number-of-document-tree-child-browsing-contexts
size_t Window::document_tree_child_browsing_context_count() const
{
diff --git a/Userland/Libraries/LibWeb/HTML/Window.h b/Userland/Libraries/LibWeb/HTML/Window.h
index 96f6599826..25195868d5 100644
--- a/Userland/Libraries/LibWeb/HTML/Window.h
+++ b/Userland/Libraries/LibWeb/HTML/Window.h
@@ -186,6 +186,8 @@ public:
WebIDL::ExceptionOr<JS::NonnullGCPtr<Crypto::Crypto>> crypto();
+ WebIDL::ExceptionOr<JS::NonnullGCPtr<CustomElementRegistry>> custom_elements();
+
private:
explicit Window(JS::Realm&);
@@ -216,6 +218,10 @@ private:
JS::GCPtr<Navigator> m_navigator;
JS::GCPtr<Location> m_location;
+ // 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.
+ JS::GCPtr<CustomElementRegistry> m_custom_element_registry;
+
AnimationFrameCallbackDriver m_animation_frame_callback_driver;
// https://w3c.github.io/requestidlecallback/#dfn-list-of-idle-request-callbacks
diff --git a/Userland/Libraries/LibWeb/HTML/Window.idl b/Userland/Libraries/LibWeb/HTML/Window.idl
index c605544a9e..5537e5b6ef 100644
--- a/Userland/Libraries/LibWeb/HTML/Window.idl
+++ b/Userland/Libraries/LibWeb/HTML/Window.idl
@@ -6,6 +6,7 @@
#import <DOM/EventTarget.idl>
#import <HighResolutionTime/Performance.idl>
#import <HTML/AnimationFrameProvider.idl>
+#import <HTML/CustomElements/CustomElementRegistry.idl>
#import <HTML/Navigator.idl>
#import <HTML/WindowLocalStorage.idl>
#import <HTML/WindowOrWorkerGlobalScope.idl>
@@ -22,6 +23,7 @@ interface Window : EventTarget {
attribute DOMString name;
[PutForwards=href, LegacyUnforgeable] readonly attribute Location location;
readonly attribute History history;
+ readonly attribute CustomElementRegistry customElements;
undefined focus();
// other browsing contexts
diff --git a/Userland/Libraries/LibWeb/Loader/FrameLoader.cpp b/Userland/Libraries/LibWeb/Loader/FrameLoader.cpp
index e31e8f02b3..c87217d728 100644
--- a/Userland/Libraries/LibWeb/Loader/FrameLoader.cpp
+++ b/Userland/Libraries/LibWeb/Loader/FrameLoader.cpp
@@ -23,6 +23,7 @@
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/Loader/FrameLoader.h>
#include <LibWeb/Loader/ResourceLoader.h>
+#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Platform/ImageCodecPlugin.h>
#include <LibWeb/XML/XMLDocumentBuilder.h>
@@ -104,21 +105,21 @@ static bool build_markdown_document(DOM::Document& document, ByteBuffer const& d
static bool build_text_document(DOM::Document& document, ByteBuffer const& data)
{
- auto html_element = document.create_element("html").release_value();
+ auto html_element = DOM::create_element(document, HTML::TagNames::html, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(document.append_child(html_element));
- auto head_element = document.create_element("head").release_value();
+ auto head_element = DOM::create_element(document, HTML::TagNames::head, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(html_element->append_child(head_element));
- auto title_element = document.create_element("title").release_value();
+ auto title_element = DOM::create_element(document, HTML::TagNames::title, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(head_element->append_child(title_element));
auto title_text = document.create_text_node(document.url().basename());
MUST(title_element->append_child(title_text));
- auto body_element = document.create_element("body").release_value();
+ auto body_element = DOM::create_element(document, HTML::TagNames::body, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(html_element->append_child(body_element));
- auto pre_element = document.create_element("pre").release_value();
+ auto pre_element = DOM::create_element(document, HTML::TagNames::pre, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(body_element->append_child(pre_element));
MUST(pre_element->append_child(document.create_text_node(DeprecatedString::copy(data))));
@@ -135,22 +136,22 @@ static bool build_image_document(DOM::Document& document, ByteBuffer const& data
if (!bitmap)
return false;
- auto html_element = document.create_element("html").release_value();
+ auto html_element = DOM::create_element(document, HTML::TagNames::html, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(document.append_child(html_element));
- auto head_element = document.create_element("head").release_value();
+ auto head_element = DOM::create_element(document, HTML::TagNames::head, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(html_element->append_child(head_element));
- auto title_element = document.create_element("title").release_value();
+ auto title_element = DOM::create_element(document, HTML::TagNames::title, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(head_element->append_child(title_element));
auto basename = LexicalPath::basename(document.url().path());
auto title_text = document.heap().allocate<DOM::Text>(document.realm(), document, DeprecatedString::formatted("{} [{}x{}]", basename, bitmap->width(), bitmap->height())).release_allocated_value_but_fixme_should_propagate_errors();
MUST(title_element->append_child(*title_text));
- auto body_element = document.create_element("body").release_value();
+ auto body_element = DOM::create_element(document, HTML::TagNames::body, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(html_element->append_child(body_element));
- auto image_element = document.create_element("img").release_value();
+ auto image_element = DOM::create_element(document, HTML::TagNames::img, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
MUST(image_element->set_attribute(HTML::AttributeNames::src, document.url().to_deprecated_string()));
MUST(body_element->append_child(image_element));
diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake
index 38c558cc0f..8c162a1911 100644
--- a/Userland/Libraries/LibWeb/idl_files.cmake
+++ b/Userland/Libraries/LibWeb/idl_files.cmake
@@ -71,6 +71,7 @@ libweb_js_bindings(HTML/CanvasGradient)
libweb_js_bindings(HTML/CanvasPattern)
libweb_js_bindings(HTML/CanvasRenderingContext2D)
libweb_js_bindings(HTML/CloseEvent)
+libweb_js_bindings(HTML/CustomElements/CustomElementRegistry)
libweb_js_bindings(HTML/DOMParser)
libweb_js_bindings(HTML/DOMStringMap)
libweb_js_bindings(HTML/ErrorEvent)