ladybird/Userland/Libraries/LibWeb/DOM/TreeWalker.cpp
Shannon Booth bad44f8fc9 LibWeb: Remove Bindings/Forward.h from LibWeb/Forward.h
This was resulting in a whole lot of rebuilding whenever a new IDL
interface was added.

Instead, just directly include the prototype in every C++ file which
needs it. While we only really need a forward declaration in each cpp
file; including the full prototype header (which itself only includes
LibJS/Object.h, which is already transitively brought in by
PlatformObject) - it seems like a small price to pay compared to what
feels like a full rebuild of LibWeb whenever a new IDL file is added.

Given all of these includes are only needed for the ::initialize
method, there is probably a smart way of avoiding this problem
altogether. I've considered both using some macro trickery or generating
these functions somehow instead.
2024-04-27 18:29:35 -04:00

391 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/TreeWalkerPrototype.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/DOM/NodeFilter.h>
#include <LibWeb/DOM/TreeWalker.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/DOMException.h>
namespace Web::DOM {
JS_DEFINE_ALLOCATOR(TreeWalker);
TreeWalker::TreeWalker(Node& root)
: PlatformObject(root.realm())
, m_root(root)
, m_current(root)
{
}
TreeWalker::~TreeWalker() = default;
void TreeWalker::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(TreeWalker);
}
void TreeWalker::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_filter);
visitor.visit(m_root);
visitor.visit(m_current);
}
// https://dom.spec.whatwg.org/#dom-document-createtreewalker
JS::NonnullGCPtr<TreeWalker> TreeWalker::create(Node& root, unsigned what_to_show, JS::GCPtr<NodeFilter> filter)
{
// 1. Let walker be a new TreeWalker object.
// 2. Set walkers root and walkers current to root.
auto& realm = root.realm();
auto walker = realm.heap().allocate<TreeWalker>(realm, root);
// 3. Set walkers whatToShow to whatToShow.
walker->m_what_to_show = what_to_show;
// 4. Set walkers filter to filter.
walker->m_filter = filter;
// 5. Return walker.
return walker;
}
// https://dom.spec.whatwg.org/#dom-treewalker-currentnode
JS::NonnullGCPtr<Node> TreeWalker::current_node() const
{
return *m_current;
}
// https://dom.spec.whatwg.org/#dom-treewalker-currentnode
void TreeWalker::set_current_node(Node& node)
{
m_current = node;
}
// https://dom.spec.whatwg.org/#dom-treewalker-parentnode
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::parent_node()
{
// 1. Let node be thiss current.
JS::GCPtr<Node> node = m_current;
// 2. While node is non-null and is not thiss root:
while (node && node != m_root) {
// 1. Set node to nodes parent.
node = node->parent();
// 2. If node is non-null and filtering node within this returns FILTER_ACCEPT,
// then set thiss current to node and return node.
if (node) {
auto result = TRY(filter(*node));
if (result == NodeFilter::Result::FILTER_ACCEPT) {
m_current = *node;
return node;
}
}
}
return nullptr;
}
// https://dom.spec.whatwg.org/#dom-treewalker-firstchild
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::first_child()
{
return traverse_children(ChildTraversalType::First);
}
// https://dom.spec.whatwg.org/#dom-treewalker-lastchild
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::last_child()
{
return traverse_children(ChildTraversalType::Last);
}
// https://dom.spec.whatwg.org/#dom-treewalker-previoussibling
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::previous_sibling()
{
return traverse_siblings(SiblingTraversalType::Previous);
}
// https://dom.spec.whatwg.org/#dom-treewalker-nextsibling
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::next_sibling()
{
return traverse_siblings(SiblingTraversalType::Next);
}
// https://dom.spec.whatwg.org/#dom-treewalker-previousnode
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::previous_node()
{
// 1. Let node be thiss current.
JS::NonnullGCPtr<Node> node = m_current;
// 2. While node is not thiss root:
while (node != m_root) {
// 1. Let sibling be nodes previous sibling.
JS::GCPtr<Node> sibling = node->previous_sibling();
// 2. While sibling is non-null:
while (sibling) {
// 1. Set node to sibling.
node = *sibling;
// 2. Let result be the result of filtering node within this.
auto result = TRY(filter(*node));
// 3. While result is not FILTER_REJECT and node has a child:
while (result != NodeFilter::Result::FILTER_REJECT && node->has_children()) {
// 1. Set node to nodes last child.
node = *node->last_child();
// 2. Set result to the result of filtering node within this.
result = TRY(filter(*node));
}
// 4. If result is FILTER_ACCEPT, then set thiss current to node and return node.
if (result == NodeFilter::Result::FILTER_ACCEPT) {
m_current = node;
return node;
}
// 5. Set sibling to nodes previous sibling.
sibling = node->previous_sibling();
}
// 3. If node is thiss root or nodes parent is null, then return null.
if (node == m_root || !node->parent())
return nullptr;
// 4. Set node to nodes parent.
node = *node->parent();
// 5. If the return value of filtering node within this is FILTER_ACCEPT, then set thiss current to node and return node.
if (TRY(filter(*node)) == NodeFilter::Result::FILTER_ACCEPT) {
m_current = node;
return node;
}
}
// 3. Return null.
return nullptr;
}
// https://dom.spec.whatwg.org/#dom-treewalker-nextnode
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::next_node()
{
// 1. Let node be thiss current.
JS::NonnullGCPtr<Node> node = m_current;
// 2. Let result be FILTER_ACCEPT.
auto result = NodeFilter::Result::FILTER_ACCEPT;
// 3. While true:
while (true) {
// 1. While result is not FILTER_REJECT and node has a child:
while (result != NodeFilter::Result::FILTER_REJECT && node->has_children()) {
// 1. Set node to its first child.
node = *node->first_child();
// 2. Set result to the result of filtering node within this.
result = TRY(filter(*node));
// 3. If result is FILTER_ACCEPT, then set thiss current to node and return node.
if (result == NodeFilter::Result::FILTER_ACCEPT) {
m_current = *node;
return node;
}
}
// 2. Let sibling be null.
JS::GCPtr<Node> sibling = nullptr;
// 3. Let temporary be node.
JS::GCPtr<Node> temporary = node;
// 4. While temporary is non-null:
while (temporary) {
// 1. If temporary is thiss root, then return null.
if (temporary == m_root)
return nullptr;
// 2. Set sibling to temporarys next sibling.
sibling = temporary->next_sibling();
// 3. If sibling is non-null, then set node to sibling and break.
if (sibling) {
node = *sibling;
break;
}
// 4. Set temporary to temporarys parent.
temporary = temporary->parent();
// NON-STANDARD: If temporary is null, then return null.
// This prevents us from infinite looping if the current node is not connected.
// Spec bug: https://github.com/whatwg/dom/issues/1102
if (temporary == nullptr) {
return nullptr;
}
}
// 5. Set result to the result of filtering node within this.
result = TRY(filter(*node));
// 6. If result is FILTER_ACCEPT, then set thiss current to node and return node.
if (result == NodeFilter::Result::FILTER_ACCEPT) {
m_current = *node;
return node;
}
}
}
// https://dom.spec.whatwg.org/#concept-node-filter
JS::ThrowCompletionOr<NodeFilter::Result> TreeWalker::filter(Node& node)
{
// 1. If traversers active flag is set, then throw an "InvalidStateError" DOMException.
if (m_active)
return throw_completion(WebIDL::InvalidStateError::create(realm(), "NodeIterator is already active"_fly_string));
// 2. Let n be nodes nodeType attribute value 1.
auto n = node.node_type() - 1;
// 3. If the nth bit (where 0 is the least significant bit) of traversers whatToShow is not set, then return FILTER_SKIP.
if (!(m_what_to_show & (1u << n)))
return NodeFilter::Result::FILTER_SKIP;
// 4. If traversers filter is null, then return FILTER_ACCEPT.
if (!m_filter)
return NodeFilter::Result::FILTER_ACCEPT;
// 5. Set traversers active flag.
m_active = true;
// 6. Let result be the return value of call a user objects operation with traversers filter, "acceptNode", and « node ».
// If this throws an exception, then unset traversers active flag and rethrow the exception.
auto result = WebIDL::call_user_object_operation(m_filter->callback(), "acceptNode"_string, {}, &node);
if (result.is_abrupt()) {
m_active = false;
return result;
}
// 7. Unset traversers active flag.
m_active = false;
// 8. Return result.
auto result_value = TRY(result.value()->to_i32(vm()));
return static_cast<NodeFilter::Result>(result_value);
}
// https://dom.spec.whatwg.org/#concept-traverse-children
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::traverse_children(ChildTraversalType type)
{
// 1. Let node be walkers current.
JS::GCPtr<Node> node = m_current;
// 2. Set node to nodes first child if type is first, and nodes last child if type is last.
node = type == ChildTraversalType::First ? node->first_child() : node->last_child();
// 3. While node is non-null:
while (node) {
// 1. Let result be the result of filtering node within walker.
auto result = TRY(filter(*node));
// 2. If result is FILTER_ACCEPT, then set walkers current to node and return node.
if (result == NodeFilter::Result::FILTER_ACCEPT) {
m_current = *node;
return node;
}
// 3. If result is FILTER_SKIP, then:
if (result == NodeFilter::Result::FILTER_SKIP) {
// 1. Let child be nodes first child if type is first, and nodes last child if type is last.
JS::GCPtr<Node> child = type == ChildTraversalType::First ? node->first_child() : node->last_child();
// 2. If child is non-null, then set node to child and continue.
if (child) {
node = child;
continue;
}
}
// 4. While node is non-null:
while (node) {
// 1. Let sibling be nodes next sibling if type is first, and nodes previous sibling if type is last.
JS::GCPtr<Node> sibling = type == ChildTraversalType::First ? node->next_sibling() : node->previous_sibling();
// 2. If sibling is non-null, then set node to sibling and break.
if (sibling) {
node = sibling;
break;
}
// 3. Let parent be nodes parent.
JS::GCPtr<Node> parent = node->parent();
// 4. If parent is null, walkers root, or walkers current, then return null.
if (!parent || parent == m_root || parent == m_current)
return nullptr;
// 5. Set node to parent.
node = parent;
}
}
// 4. Return null.
return nullptr;
}
// https://dom.spec.whatwg.org/#concept-traverse-siblings
JS::ThrowCompletionOr<JS::GCPtr<Node>> TreeWalker::traverse_siblings(SiblingTraversalType type)
{
// 1. Let node be walkers current.
JS::GCPtr<Node> node = m_current;
// 2. If node is root, then return null.
if (node == m_root)
return nullptr;
// 3. While true:
while (true) {
// 1. Let sibling be nodes next sibling if type is next, and nodes previous sibling if type is previous.
JS::GCPtr<Node> sibling = type == SiblingTraversalType::Next ? node->next_sibling() : node->previous_sibling();
// 2. While sibling is non-null:
while (sibling) {
// 1. Set node to sibling.
node = sibling;
// 2. Let result be the result of filtering node within walker.
auto result = TRY(filter(*node));
// 3. If result is FILTER_ACCEPT, then set walkers current to node and return node.
if (result == NodeFilter::Result::FILTER_ACCEPT) {
m_current = *node;
return node;
}
// 4. Set sibling to nodes first child if type is next, and nodes last child if type is previous.
sibling = type == SiblingTraversalType::Next ? node->first_child() : node->last_child();
// 5. If result is FILTER_REJECT or sibling is null, then set sibling to nodes next sibling if type is next, and nodes previous sibling if type is previous.
if (result == NodeFilter::Result::FILTER_REJECT || !sibling)
sibling = type == SiblingTraversalType::Next ? node->next_sibling() : node->previous_sibling();
}
// 3. Set node to nodes parent.
node = node->parent();
// 4. If node is null or walkers root, then return null.
if (!node || node == m_root)
return nullptr;
// 5. If the return value of filtering node within walker is FILTER_ACCEPT, then return null.
if (TRY(filter(*node)) == NodeFilter::Result::FILTER_ACCEPT)
return nullptr;
}
}
}