LibWeb/DOM: Update node cloning to latest spec

Main difference is that a chunk of the "clone a node" steps are pulled
out into a "clone a single node" algorithm.

Reflects these spec PRs:
https://github.com/whatwg/dom/pull/1332
https://github.com/whatwg/dom/pull/1334

Though this code is quite old so there may also be older spec changes
included here.
This commit is contained in:
Sam Atkins 2025-01-03 15:24:30 +00:00 committed by Tim Ledbetter
parent 331b1b22f5
commit c60ad5b0b8
Notes: github-actions[bot] 2025-01-04 12:15:26 +00:00
2 changed files with 162 additions and 115 deletions

View file

@ -1026,150 +1026,194 @@ WebIDL::ExceptionOr<GC::Ref<Node>> Node::replace_child(GC::Ref<Node> node, GC::R
}
// https://dom.spec.whatwg.org/#concept-node-clone
WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_node(Document* document, bool clone_children)
WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_node(Document* document, bool subtree, Node* parent)
{
// 1. If document is not given, let document be nodes node document.
// To clone a node given a node node and an optional document document (default nodes node document),
// boolean subtree (default false), and node-or-null parent (default null):
if (!document)
document = m_document.ptr();
GC::Ptr<Node> copy;
document = m_document;
// 2. If node is an element, then:
if (is<Element>(this)) {
// 1. Let copy be the result of creating an element, given document, nodes local name, nodes namespace, nodes namespace prefix, and nodes is value, with the synchronous custom elements flag unset.
// 1. Assert: node is not a document or node is document.
VERIFY(!is_document() || this == document);
// 2. Let copy be the result of cloning a single node given node and document.
auto copy = TRY(clone_single_node(*document));
// 3. Run any cloning steps defined for node in other applicable specifications and pass node, copy, and subtree as parameters.
TRY(cloned(*copy, subtree));
// 4. If parent is non-null, then append copy to parent.
if (parent)
TRY(parent->append_child(copy));
// 5. If subtree is true, then for each child of nodes children, in tree order:
// clone a node given child with document set to document, subtree set to subtree, and parent set to copy.
if (subtree) {
for (auto child = first_child(); child; child = child->next_sibling()) {
TRY(child->clone_node(document, subtree, copy));
}
}
// 6. If node is an element, node is a shadow host, and nodes shadow roots clonable is true:
if (is_element()) {
auto& node_element = verify_cast<Element>(*this);
if (node_element.is_shadow_host() && node_element.shadow_root()->clonable()) {
// 1. Assert: copy is not a shadow host.
auto& copy_element = verify_cast<Element>(*copy);
VERIFY(!copy_element.is_shadow_host());
// 2. Attach a shadow root with copy, nodes shadow roots mode, true, nodes shadow roots serializable, nodes shadow roots delegates focus, and nodes shadow roots slot assignment.
TRY(copy_element.attach_a_shadow_root(node_element.shadow_root()->mode(), true, node_element.shadow_root()->serializable(), node_element.shadow_root()->delegates_focus(), node_element.shadow_root()->slot_assignment()));
// 3. Set copys shadow roots declarative to nodes shadow roots declarative.
copy_element.shadow_root()->set_declarative(node_element.shadow_root()->declarative());
// 4. For each child of nodes shadow roots children, in tree order:
// clone a node given child with document set to document, subtree set to subtree, and parent set to copys shadow root.
for (auto child = node_element.shadow_root()->first_child(); child; child = child->next_sibling()) {
TRY(child->clone_node(document, subtree, copy_element.shadow_root()));
}
}
}
// 7. Return copy.
return GC::Ref { *copy };
}
// https://dom.spec.whatwg.org/#clone-a-single-node
WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_single_node(Document& document)
{
// To clone a single node given a node node and document document:
// 1. Let copy be null.
GC::Ptr<Node> copy = nullptr;
// 2. If node is an element:
if (is_element()) {
// 1. Set copy to the result of creating an element, given document, nodes local name, nodes namespace, nodes namespace prefix, and nodes is value.
auto& element = *verify_cast<Element>(this);
auto element_copy = DOM::create_element(*document, element.local_name(), element.namespace_uri(), element.prefix(), element.is_value(), false).release_value_but_fixme_should_propagate_errors();
auto element_copy = TRY(DOM::create_element(document, element.local_name(), element.namespace_uri(), element.prefix(), element.is_value()));
// 2. For each attribute in nodes attribute list:
// 2. For each attribute of nodes attribute list:
element.for_each_attribute([&](auto& name, auto& value) {
// 1. Let copyAttribute be a clone of attribute.
// FIXME: 1. Let copyAttribute be the result of cloning a single node given attribute and document.
// 2. Append copyAttribute to copy.
element_copy->append_attribute(name, value);
});
copy = move(element_copy);
}
// 3. Otherwise, let copy be a node that implements the same interfaces as node, and fulfills these additional requirements, switching on the interface node implements:
else if (is<Document>(this)) {
// Document
auto document_ = verify_cast<Document>(this);
// 3. Otherwise, set copy to a node that implements the same interfaces as node, and fulfills these additional requirements, switching on the interface node implements:
else {
if (is_document()) {
// -> Document
auto& document_ = verify_cast<Document>(*this);
auto document_copy = [&] -> GC::Ref<Document> {
switch (document_->document_type()) {
switch (document_.document_type()) {
case Document::Type::XML:
return XMLDocument::create(realm(), document_->url());
return XMLDocument::create(realm(), document_.url());
case Document::Type::HTML:
return HTML::HTMLDocument::create(realm(), document_->url());
return HTML::HTMLDocument::create(realm(), document_.url());
default:
return Document::create(realm(), document_->url());
return Document::create(realm(), document_.url());
}
}();
// Set copys encoding, content type, URL, origin, type, and mode to those of node.
document_copy->set_encoding(document_->encoding());
document_copy->set_content_type(document_->content_type());
document_copy->set_url(document_->url());
document_copy->set_origin(document_->origin());
document_copy->set_document_type(document_->document_type());
document_copy->set_quirks_mode(document_->mode());
document_copy->set_encoding(document_.encoding());
document_copy->set_content_type(document_.content_type());
document_copy->set_url(document_.url());
document_copy->set_origin(document_.origin());
document_copy->set_document_type(document_.document_type());
document_copy->set_quirks_mode(document_.mode());
copy = move(document_copy);
} else if (is<DocumentType>(this)) {
// DocumentType
auto document_type = verify_cast<DocumentType>(this);
auto document_type_copy = realm().create<DocumentType>(*document);
} else if (is_document_type()) {
// -> DocumentType
auto& document_type = verify_cast<DocumentType>(*this);
auto document_type_copy = realm().create<DocumentType>(document);
// Set copys name, public ID, and system ID to those of node.
document_type_copy->set_name(document_type->name());
document_type_copy->set_public_id(document_type->public_id());
document_type_copy->set_system_id(document_type->system_id());
document_type_copy->set_name(document_type.name());
document_type_copy->set_public_id(document_type.public_id());
document_type_copy->set_system_id(document_type.system_id());
copy = move(document_type_copy);
} else if (is<Attr>(this)) {
// Attr
} else if (is_attribute()) {
// -> Attr
// Set copys namespace, namespace prefix, local name, and value to those of node.
auto& attr = static_cast<Attr&>(*this);
copy = attr.clone(*document);
} else if (is<Text>(this)) {
// Text
auto& text = static_cast<Text&>(*this);
auto& attr = verify_cast<Attr>(*this);
copy = attr.clone(document);
} else if (is_text()) {
// -> Text
auto& text = verify_cast<Text>(*this);
// Set copys data to that of node.
copy = [&]() -> GC::Ref<Text> {
switch (type()) {
case NodeType::TEXT_NODE:
return realm().create<Text>(*document, text.data());
return realm().create<Text>(document, text.data());
case NodeType::CDATA_SECTION_NODE:
return realm().create<CDATASection>(*document, text.data());
return realm().create<CDATASection>(document, text.data());
default:
VERIFY_NOT_REACHED();
}
}();
} else if (is<Comment>(this)) {
// Comment
auto comment = verify_cast<Comment>(this);
} else if (is_comment()) {
// -> Comment
auto& comment = verify_cast<Comment>(*this);
// Set copys data to that of node.
auto comment_copy = realm().create<Comment>(*document, comment->data());
auto comment_copy = realm().create<Comment>(document, comment.data());
copy = move(comment_copy);
} else if (is<ProcessingInstruction>(this)) {
// ProcessingInstruction
auto processing_instruction = verify_cast<ProcessingInstruction>(this);
// -> ProcessingInstruction
auto& processing_instruction = verify_cast<ProcessingInstruction>(*this);
// Set copys target and data to those of node.
auto processing_instruction_copy = realm().create<ProcessingInstruction>(*document, processing_instruction->data(), processing_instruction->target());
copy = processing_instruction_copy;
auto processing_instruction_copy = realm().create<ProcessingInstruction>(document, processing_instruction.data(), processing_instruction.target());
copy = move(processing_instruction_copy);
}
// Otherwise, Do nothing.
// -> Otherwise
// Do nothing.
else if (is<DocumentFragment>(this)) {
copy = realm().create<DocumentFragment>(*document);
}
// FIXME: 4. Set copys node document and document to copy, if copy is a document, and set copys node document to document otherwise.
// 5. Run any cloning steps defined for node in other applicable specifications and pass copy, node, document and the clone children flag if set, as parameters.
TRY(cloned(*copy, clone_children));
// 6. If the clone children flag is set, clone all the children of node and append them to copy, with document as specified and the clone children flag being set.
if (clone_children) {
for (auto child = first_child(); child; child = child->next_sibling()) {
TRY(copy->append_child(TRY(child->clone_node(document, true))));
copy = realm().create<DocumentFragment>(document);
} else {
dbgln("Missing code for cloning a '{}' node. Please add it to Node::clone_single_node()", class_name());
VERIFY_NOT_REACHED();
}
}
// 7. If node is a shadow host whose shadow roots clonable is true:
if (is_element() && static_cast<Element const&>(*this).is_shadow_host() && static_cast<Element const&>(*this).shadow_root()->clonable()) {
// 1. Assert: copy is not a shadow host.
VERIFY(!copy->is_element() || !static_cast<Element const&>(*copy).is_shadow_host());
// 4. Assert: copy is a node.
VERIFY(copy);
// 2. Run attach a shadow root with copy, nodes shadow roots mode, true, nodes shadow roots serializable,
// nodes shadow roots delegates focus, and nodes shadow roots slot assignment.
auto& node_shadow_root = *static_cast<Element&>(*this).shadow_root();
TRY(static_cast<Element&>(*copy).attach_a_shadow_root(node_shadow_root.mode(), true, node_shadow_root.serializable(), node_shadow_root.delegates_focus(), node_shadow_root.slot_assignment()));
// 5. If node is a document, then set document to copy.
Document& document_to_use = is_document()
? static_cast<Document&>(*copy)
: document;
// 3. Set copys shadow roots declarative to nodes shadow roots declarative.
static_cast<Element&>(*copy).shadow_root()->set_declarative(node_shadow_root.declarative());
// 4. For each child child of nodes shadow root, in tree order:
// append the result of cloning child with document and the clone children flag set, to copys shadow root.
for (auto child = node_shadow_root.first_child(); child; child = child->next_sibling()) {
TRY(static_cast<Element&>(*copy).shadow_root()->append_child(TRY(child->clone_node(document, true))));
}
}
// 6. Set copys node document to document.
copy->set_document(document_to_use);
// 7. Return copy.
VERIFY(copy);
return GC::Ref { *copy };
}
// https://dom.spec.whatwg.org/#dom-node-clonenode
WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_node_binding(bool deep)
WebIDL::ExceptionOr<GC::Ref<Node>> Node::clone_node_binding(bool subtree)
{
// 1. If this is a shadow root, then throw a "NotSupportedError" DOMException.
if (is<ShadowRoot>(*this))
return WebIDL::NotSupportedError::create(realm(), "Cannot clone shadow root"_string);
// 2. Return a clone of this, with the clone children flag set if deep is true.
return clone_node(nullptr, deep);
// 2. Return the result of cloning a node given this with subtree set to subtree.
return clone_node(nullptr, subtree);
}
void Node::set_document(Badge<Document>, Document& document)
{
set_document(document);
}
void Node::set_document(Document& document)
{
if (m_document.ptr() == &document)
return;

View file

@ -192,8 +192,9 @@ public:
WebIDL::ExceptionOr<GC::Ref<Node>> replace_child(GC::Ref<Node> node, GC::Ref<Node> child);
WebIDL::ExceptionOr<GC::Ref<Node>> clone_node(Document* document = nullptr, bool clone_children = false);
WebIDL::ExceptionOr<GC::Ref<Node>> clone_node_binding(bool deep);
WebIDL::ExceptionOr<GC::Ref<Node>> clone_node(Document* document = nullptr, bool subtree = false, Node* parent = nullptr);
WebIDL::ExceptionOr<GC::Ref<Node>> clone_single_node(Document&);
WebIDL::ExceptionOr<GC::Ref<Node>> clone_node_binding(bool subtree);
// NOTE: This is intended for the JS bindings.
bool has_child_nodes() const { return has_children(); }
@ -742,6 +743,8 @@ protected:
Node(JS::Realm&, Document&, NodeType);
Node(Document&, NodeType);
void set_document(Document&);
virtual void visit_edges(Cell::Visitor&) override;
virtual void finalize() override;