LibWeb: Make the node mutation algorithms more spec compliant

The mutation algorithms now more closely follow the spec and
fixes some assertion failures in tests such as Acid3 and Dromaeo.

The main thing that is missing right now is passing exceptions to the
bindings layer. This is because of issue #6075. I spent a while trying
to work it out and got so frustrated I just left it as a FIXME. Besides
that, the algorithms bail at the appropriate points.

This also makes the adopting steps in the document more spec compliant
as it's needed by the insertion algorithm. While I was at it, I added
the adoptNode IDL binding.

This adds a bunch of ancestor/descendant checks to TreeNode as well.
I moved the "remove_all_children" function to Node as it needs to use
the full remove algorithm instead of simply removing it from
the child list.
This commit is contained in:
Luke 2021-04-06 19:34:49 +01:00 committed by Andreas Kling
parent 5b5d7857e3
commit 5beacf08a2
Notes: sideshowbarker 2024-07-18 20:42:45 +09:00
8 changed files with 305 additions and 55 deletions

View file

@ -43,6 +43,7 @@
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/ExceptionOr.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/DOM/Window.h>
#include <LibWeb/Dump.h>
@ -124,7 +125,7 @@ void Document::removed_last_ref()
VERIFY(&node.document() == this);
VERIFY(!node.is_document());
if (node.parent())
node.parent()->remove_child(node);
node.remove();
}
}
@ -282,9 +283,7 @@ void Document::set_title(const String& title)
head_element->append_child(*title_element);
}
while (RefPtr<Node> child = title_element->first_child())
title_element->remove_child(child.release_nonnull());
title_element->remove_all_children(true);
title_element->append_child(adopt(*new Text(*this, title)));
if (auto* page = this->page()) {
@ -659,12 +658,52 @@ NonnullRefPtrVector<HTML::HTMLScriptElement> Document::take_scripts_to_execute_a
return move(m_scripts_to_execute_as_soon_as_possible);
}
void Document::adopt_node(Node& subtree_root)
// https://dom.spec.whatwg.org/#concept-node-adopt
void Document::adopt_node(Node& node)
{
subtree_root.for_each_in_inclusive_subtree([&](auto& node) {
node.set_document({}, *this);
return IterationDecision::Continue;
});
auto& old_document = node.document();
if (node.parent())
node.remove();
if (&old_document != this) {
// FIXME: This should be shadow-including.
node.for_each_in_inclusive_subtree([&](auto& inclusive_descendant) {
inclusive_descendant.set_document({}, *this);
// FIXME: If inclusiveDescendant is an element, then set the node document of each attribute in inclusiveDescendants attribute list to document.
return IterationDecision::Continue;
});
// FIXME: For each inclusiveDescendant in nodes 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.
// FIXME: This should be shadow-including.
node.for_each_in_inclusive_subtree([&](auto& inclusive_descendant) {
inclusive_descendant.adopted_from(old_document);
return IterationDecision::Continue;
});
}
}
// https://dom.spec.whatwg.org/#dom-document-adoptnode
NonnullRefPtr<Node> Document::adopt_node_binding(NonnullRefPtr<Node> node)
{
if (is<Document>(*node)) {
dbgln("Document::adopt_node_binding: Cannot adopt a document into a document (FIXME: throw as NotSupportedError exception, see issue #6075");
return node;
}
if (is<ShadowRoot>(*node)) {
dbgln("Document::adopt_node_binding: Cannot adopt a shadow root into a document (FIXME: throw as HierarchyRequestError exception, see issue #6075");
return node;
}
if (is<DocumentFragment>(*node) && downcast<DocumentFragment>(*node).host())
return node;
adopt_node(*node);
return node;
}
const DocumentType* Document::doctype() const

View file

@ -188,6 +188,7 @@ public:
void set_quirks_mode(QuirksMode mode) { m_quirks_mode = mode; }
void adopt_node(Node&);
NonnullRefPtr<Node> adopt_node_binding(NonnullRefPtr<Node>);
const DocumentType* doctype() const;
const String& compat_mode() const;

View file

@ -31,6 +31,8 @@ interface Document : Node {
Comment createComment(DOMString data);
Range createRange();
[CEReactions, ImplementedAs=adopt_node_binding] Node adoptNode(Node node);
[ImplementedAs=style_sheets_for_bindings] readonly attribute StyleSheetList styleSheets;
readonly attribute DOMString compatMode;

View file

@ -30,11 +30,14 @@
#include <LibWeb/Bindings/EventWrapper.h>
#include <LibWeb/Bindings/NodeWrapper.h>
#include <LibWeb/Bindings/NodeWrapperFactory.h>
#include <LibWeb/DOM/Comment.h>
#include <LibWeb/DOM/DocumentType.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/EventDispatcher.h>
#include <LibWeb/DOM/EventListener.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/DOM/ProcessingInstruction.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/Layout/InitialContainingBlockBox.h>
@ -179,38 +182,191 @@ const Element* Node::parent_element() const
return downcast<Element>(parent());
}
RefPtr<Node> Node::append_child(NonnullRefPtr<Node> node, bool notify)
// https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
ExceptionOr<void> Node::ensure_pre_insertion_validity(NonnullRefPtr<Node> node, RefPtr<Node> child) const
{
if (node->parent())
node->parent()->remove_child(node);
if (&node->document() != &document())
document().adopt_node(node);
TreeNode<Node>::append_child(node, notify);
return node;
}
if (!is<Document>(this) && !is<DocumentFragment>(this) && !is<Element>(this))
return DOM::HierarchyRequestError::create("Can only insert into a document, document fragment or element");
RefPtr<Node> Node::remove_child(NonnullRefPtr<Node> node)
{
TreeNode<Node>::remove_child(node);
return node;
}
if (node->is_host_including_inclusive_ancestor_of(*this))
return DOM::HierarchyRequestError::create("New node is an ancestor of this node");
RefPtr<Node> Node::insert_before(NonnullRefPtr<Node> node, RefPtr<Node> child, bool notify)
{
if (!child)
return append_child(move(node), notify);
if (child->parent_node() != this) {
dbgln("FIXME: Trying to insert_before() a bogus child");
return nullptr;
if (child && child->parent() != this)
return DOM::NotFoundError::create("This node is not the parent of the given child");
// FIXME: All the following "Invalid node type for insertion" messages could be more descriptive.
if (!is<DocumentFragment>(*node) && !is<DocumentType>(*node) && !is<Element>(*node) && !is<Text>(*node) && !is<Comment>(*node) && !is<ProcessingInstruction>(*node))
return DOM::HierarchyRequestError::create("Invalid node type for insertion");
if ((is<Text>(*node) && is<Document>(this)) || (is<DocumentType>(*node) && !is<Document>(this)))
return DOM::HierarchyRequestError::create("Invalid node type for insertion");
if (is<Document>(this)) {
if (is<DocumentFragment>(*node)) {
auto node_element_child_count = node->element_child_count();
if ((node_element_child_count > 1 || node->has_child_of_type<Text>())
|| (node_element_child_count == 1 && (has_child_of_type<Element>() || is<DocumentType>(child.ptr()) /* FIXME: or child is non-null and a doctype is following child. */))) {
return DOM::HierarchyRequestError::create("Invalid node type for insertion");
}
} else if (is<Element>(*node)) {
if (has_child_of_type<Element>() || is<DocumentType>(child.ptr()) /* FIXME: or child is non-null and a doctype is following child. */)
return DOM::HierarchyRequestError::create("Invalid node type for insertion");
} else if (is<DocumentType>(*node)) {
if (has_child_of_type<DocumentType>() /* FIXME: or child is non-null and an element is preceding child */ || (!child && has_child_of_type<Element>()))
return DOM::HierarchyRequestError::create("Invalid node type for insertion");
}
}
if (node->parent())
node->parent()->remove_child(node);
if (&node->document() != &document())
document().adopt_node(node);
TreeNode<Node>::insert_before(node, child, notify);
return {};
}
// https://dom.spec.whatwg.org/#concept-node-insert
void Node::insert_before(NonnullRefPtr<Node> node, RefPtr<Node> child, bool suppress_observers)
{
NonnullRefPtrVector<Node> nodes;
if (is<DocumentFragment>(*node))
nodes = downcast<DocumentFragment>(*node).child_nodes();
else
nodes.append(node);
auto count = nodes.size();
if (count == 0)
return;
if (is<DocumentFragment>(*node)) {
node->remove_all_children(true);
// FIXME: Queue a tree mutation record for node with « », nodes, null, and null.
}
if (child) {
// FIXME: For each live range whose start node is parent and start offset is greater than childs index, increase its start offset by count.
// FIXME: For each live range whose end node is parent and end offset is greater than childs index, increase its end offset by count.
}
// FIXME: Let previousSibling be childs previous sibling or parents last child if child is null. (Currently unused so not included)
for (auto& node_to_insert : nodes) { // FIXME: In tree order
document().adopt_node(node_to_insert);
if (!child)
TreeNode<Node>::append_child(node);
else
TreeNode<Node>::insert_before(node, child);
// FIXME: If parent is a shadow host and node is a slottable, then assign a slot for node.
// FIXME: If parents 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: Run assign slottables for a tree with nodes root.
// FIXME: This should be shadow-including.
node_to_insert.for_each_in_inclusive_subtree([&](Node& inclusive_descendant) {
inclusive_descendant.inserted();
if (inclusive_descendant.is_connected()) {
// FIXME: If inclusiveDescendant is custom, then enqueue a custom element callback reaction with inclusiveDescendant,
// callback name "connectedCallback", and an empty argument list.
// FIXME: Otherwise, try to upgrade inclusiveDescendant.
}
return IterationDecision::Continue;
});
}
if (!suppress_observers) {
// FIXME: queue a tree mutation record for parent with nodes, « », previousSibling, and child.
}
children_changed();
}
// https://dom.spec.whatwg.org/#concept-node-pre-insert
NonnullRefPtr<Node> Node::pre_insert(NonnullRefPtr<Node> node, RefPtr<Node> child)
{
auto validity_result = ensure_pre_insertion_validity(node, child);
if (validity_result.is_exception()) {
dbgln("Node::pre_insert: ensure_pre_insertion_validity failed: {}. (FIXME: throw as exception, see issue #6075)", validity_result.exception().message());
return node;
}
auto reference_child = child;
if (reference_child == node)
reference_child = node->next_sibling();
insert_before(node, reference_child);
return node;
}
// https://dom.spec.whatwg.org/#concept-node-pre-remove
NonnullRefPtr<Node> Node::pre_remove(NonnullRefPtr<Node> child)
{
if (child->parent() != this) {
dbgln("Node::pre_remove: Child doesn't belong to this node. (FIXME: throw NotFoundError DOMException, see issue #6075)");
return child;
}
child->remove();
return child;
}
// https://dom.spec.whatwg.org/#concept-node-append
NonnullRefPtr<Node> Node::append_child(NonnullRefPtr<Node> node)
{
return pre_insert(node, nullptr);
}
// https://dom.spec.whatwg.org/#concept-node-remove
void Node::remove(bool suppress_observers)
{
auto* parent = TreeNode<Node>::parent();
VERIFY(parent);
// FIXME: Let index be nodes index. (Currently unused so not included)
// FIXME: For each live range whose start node is an inclusive descendant of node, set its start to (parent, index).
// FIXME: For each live range whose end node is an inclusive descendant of node, set its end to (parent, index).
// FIXME: For each live range whose start node is parent and start offset is greater than index, decrease its start offset by 1.
// FIXME: For each live range whose end node is parent and end offset is greater than index, decrease its end offset by 1.
// FIXME: For each NodeIterator object iterator whose roots node document is nodes node document, run the NodeIterator pre-removing steps given node and iterator.
// FIXME: Let oldPreviousSibling be nodes previous sibling. (Currently unused so not included)
// FIXME: Let oldNextSibling be nodes next sibling. (Currently unused so not included)
parent->remove_child(*this);
// FIXME: If node is assigned, then run assign slottables for nodes assigned slot.
// FIXME: If parents 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: If node has an inclusive descendant that is a slot, then:
// Run assign slottables for a tree with parents root.
// Run assign slottables for a tree with node.
removed_from(parent);
// FIXME: Let isParentConnected be parents connected. (Currently unused so not included)
// FIXME: 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.
// FIXME: This should be shadow-including.
for_each_in_subtree([&](Node& descendant) {
descendant.removed_from(nullptr);
// FIXME: 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.
return IterationDecision::Continue;
});
if (!suppress_observers) {
// FIXME: queue a tree mutation record for parent with « », « node », oldPreviousSibling, and oldNextSibling.
}
parent->children_changed();
}
void Node::set_document(Badge<Document>, Document& document)
{
if (m_document == &document)
@ -261,13 +417,15 @@ void Node::set_needs_style_update(bool value)
m_needs_style_update = value;
if (m_needs_style_update) {
for (auto* ancestor = parent(); ancestor; ancestor = ancestor->parent())
for (auto* ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
//dbgln("{}", ancestor->node_name());
ancestor->m_child_needs_style_update = true;
}
document().schedule_style_update();
}
}
void Node::inserted_into(Node&)
void Node::inserted()
{
set_needs_style_update(true);
}
@ -290,4 +448,26 @@ NonnullRefPtrVector<Node> Node::child_nodes() const
return nodes;
}
void Node::remove_all_children(bool suppress_observers)
{
while (RefPtr<Node> child = first_child())
child->remove(suppress_observers);
}
// https://dom.spec.whatwg.org/#concept-tree-host-including-inclusive-ancestor
bool Node::is_host_including_inclusive_ancestor_of(const Node& other) const
{
return is_inclusive_ancestor_of(other) || (is<DocumentFragment>(other.root()) && downcast<DocumentFragment>(other.root())->host() && is_inclusive_ancestor_of(*downcast<DocumentFragment>(other.root())->host().ptr()));
}
size_t Node::element_child_count() const
{
size_t count = 0;
for (auto* child = first_child(); child; child = child->next_sibling()) {
if (is<Element>(child))
++count;
}
return count;
}
}

View file

@ -33,6 +33,7 @@
#include <AK/Vector.h>
#include <LibWeb/Bindings/Wrappable.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/DOM/ExceptionOr.h>
#include <LibWeb/TreeNode.h>
namespace Web::DOM {
@ -92,9 +93,13 @@ public:
virtual bool is_editable() const;
RefPtr<Node> append_child(NonnullRefPtr<Node>, bool notify = true);
RefPtr<Node> insert_before(NonnullRefPtr<Node> node, RefPtr<Node> child, bool notify = true);
RefPtr<Node> remove_child(NonnullRefPtr<Node>);
NonnullRefPtr<Node> pre_insert(NonnullRefPtr<Node>, RefPtr<Node>);
NonnullRefPtr<Node> pre_remove(NonnullRefPtr<Node>);
NonnullRefPtr<Node> append_child(NonnullRefPtr<Node>);
void insert_before(NonnullRefPtr<Node> node, RefPtr<Node> child, bool suppress_observers = false);
void remove(bool suppress_observers = false);
void remove_all_children(bool suppress_observers = false);
// NOTE: This is intended for the JS bindings.
bool has_child_nodes() const { return has_children(); }
@ -165,6 +170,12 @@ public:
template<typename T>
bool fast_is() const = delete;
ExceptionOr<void> ensure_pre_insertion_validity(NonnullRefPtr<Node> node, RefPtr<Node> child) const;
bool is_host_including_inclusive_ancestor_of(const Node&) const;
size_t element_child_count() const;
protected:
Node(Document&, NodeType);

View file

@ -15,8 +15,8 @@ interface Node : EventTarget {
attribute DOMString textContent;
Node appendChild(Node node);
Node insertBefore(Node node, Node? child);
Node removeChild(Node child);
[ImplementedAs=pre_insert] Node insertBefore(Node node, Node? child);
[ImplementedAs=pre_remove] Node removeChild(Node child);
const unsigned short ELEMENT_NODE = 1;
const unsigned short ATTRIBUTE_NODE = 2;

View file

@ -62,14 +62,14 @@ void EditEventHandler::handle_delete(DOM::Range& range)
for (auto* parent = end->parent(); parent; parent = parent->parent())
queued_for_deletion.remove(parent);
for (auto* node : queued_for_deletion)
node->parent()->remove_child(*node);
node->remove();
// Join the parent nodes of start and end.
DOM::Node *insert_after = start, *remove_from = end, *parent_of_end = end->parent();
while (remove_from) {
auto* next_sibling = remove_from->next_sibling();
remove_from->parent()->remove_child(*remove_from);
remove_from->remove();
insert_after->parent()->insert_before(*remove_from, *insert_after);
insert_after = remove_from;
@ -77,7 +77,7 @@ void EditEventHandler::handle_delete(DOM::Range& range)
}
if (!parent_of_end->has_children()) {
if (parent_of_end->parent())
parent_of_end->parent()->remove_child(*parent_of_end);
parent_of_end->remove();
}
// Join the start and end nodes.
@ -86,7 +86,7 @@ void EditEventHandler::handle_delete(DOM::Range& range)
builder.append(end->data().substring_view(range.end_offset()));
start->set_data(builder.to_string());
start->parent()->remove_child(*end);
end->remove();
}
// FIXME: When nodes are removed from the DOM, the associated layout nodes become stale and still

View file

@ -98,14 +98,15 @@ public:
}
bool is_ancestor_of(const TreeNode&) const;
bool is_inclusive_ancestor_of(const TreeNode&) const;
bool is_descendant_of(const TreeNode&) const;
bool is_inclusive_descendant_of(const TreeNode&) const;
void append_child(NonnullRefPtr<T> node);
void prepend_child(NonnullRefPtr<T> node);
void insert_before(NonnullRefPtr<T> node, RefPtr<T> child);
void remove_child(NonnullRefPtr<T> node);
void remove_all_children();
bool is_child_allowed(const T&) const { return true; }
T* next_in_pre_order()
@ -323,6 +324,12 @@ public:
return nullptr;
}
template<typename U>
bool has_child_of_type() const
{
return first_child_of_type<U>() != nullptr;
}
template<typename U>
const U* first_ancestor_of_type() const
{
@ -365,14 +372,6 @@ private:
T* m_previous_sibling { nullptr };
};
template<typename T>
inline void TreeNode<T>::remove_all_children()
{
while (RefPtr<T> child = first_child())
remove_child(child.release_nonnull());
}
template<typename T>
inline void TreeNode<T>::remove_child(NonnullRefPtr<T> node)
{
@ -470,4 +469,22 @@ inline bool TreeNode<T>::is_ancestor_of(const TreeNode<T>& other) const
return false;
}
template<typename T>
inline bool TreeNode<T>::is_inclusive_ancestor_of(const TreeNode<T>& other) const
{
return &other == this || is_ancestor_of(other);
}
template<typename T>
inline bool TreeNode<T>::is_descendant_of(const TreeNode<T>& other) const
{
return other.is_ancestor_of(*this);
}
template<typename T>
inline bool TreeNode<T>::is_inclusive_descendant_of(const TreeNode<T>& other) const
{
return other.is_inclusive_ancestor_of(*this);
}
}