1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 21:57:43 +00:00

LibWeb: Add basic support for DOM's NodeIterator and NodeFilter

This patch adds NodeIterator (created via Document.createNodeIterator())
which allows you to iterate through all the nodes in a subtree while
filtering with a provided NodeFilter callback along the way.

This first cut implements the full API, but does not yet handle nodes
being removed from the document while referenced by the iterator. That
will be done in a subsequent patch.
This commit is contained in:
Andreas Kling 2022-03-09 14:34:32 +01:00
parent ba87f23f22
commit fabcee016f
12 changed files with 357 additions and 5 deletions

View file

@ -320,18 +320,23 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter
)~~~");
}
}
} else if (parameter.type->name == "EventListener") {
} else if (parameter.type->name.is_one_of("EventListener", "NodeFilter")) {
// FIXME: Replace this with support for callback interfaces. https://heycam.github.io/webidl/#idl-callback-interface
if (parameter.type->name == "EventListener")
scoped_generator.set("cpp_type", "IDLEventListener");
else
scoped_generator.set("cpp_type", parameter.type->name);
if (parameter.type->nullable) {
scoped_generator.append(R"~~~(
RefPtr<IDLEventListener> @cpp_name@;
RefPtr<@cpp_type@> @cpp_name@;
if (!@js_name@@js_suffix@.is_nullish()) {
if (!@js_name@@js_suffix@.is_object())
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::NotAnObject, @js_name@@js_suffix@.to_string_without_side_effects());
CallbackType callback_type(JS::make_handle(&@js_name@@js_suffix@.as_object()), HTML::incumbent_settings_object());
@cpp_name@ = adopt_ref(*new IDLEventListener(move(callback_type)));
@cpp_name@ = adopt_ref(*new @cpp_type@(move(callback_type)));
}
)~~~");
} else {
@ -340,7 +345,7 @@ static void generate_to_cpp(SourceGenerator& generator, ParameterType& parameter
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::NotAnObject, @js_name@@js_suffix@.to_string_without_side_effects());
CallbackType callback_type(JS::make_handle(&@js_name@@js_suffix@.as_object()), HTML::incumbent_settings_object());
auto @cpp_name@ = adopt_ref(*new IDLEventListener(move(callback_type)));
auto @cpp_name@ = adopt_ref(*new @cpp_type@(move(callback_type)));
)~~~");
}
} else if (IDL::is_wrappable_type(*parameter.type)) {
@ -2803,6 +2808,7 @@ void generate_prototype_implementation(IDL::Interface const& interface)
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/DOM/NodeFilter.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Window.h>

View file

@ -233,6 +233,8 @@
#include <LibWeb/Bindings/MouseEventConstructor.h>
#include <LibWeb/Bindings/MouseEventPrototype.h>
#include <LibWeb/Bindings/NodeConstructor.h>
#include <LibWeb/Bindings/NodeIteratorConstructor.h>
#include <LibWeb/Bindings/NodeIteratorPrototype.h>
#include <LibWeb/Bindings/NodeListConstructor.h>
#include <LibWeb/Bindings/NodeListPrototype.h>
#include <LibWeb/Bindings/NodePrototype.h>
@ -439,6 +441,7 @@
ADD_WINDOW_OBJECT_INTERFACE(MessageEvent) \
ADD_WINDOW_OBJECT_INTERFACE(MouseEvent) \
ADD_WINDOW_OBJECT_INTERFACE(Node) \
ADD_WINDOW_OBJECT_INTERFACE(NodeIterator) \
ADD_WINDOW_OBJECT_INTERFACE(NodeList) \
ADD_WINDOW_OBJECT_INTERFACE(PageTransitionEvent) \
ADD_WINDOW_OBJECT_INTERFACE(Performance) \

View file

@ -88,6 +88,7 @@ set(SOURCES
DOM/LiveNodeList.cpp
DOM/NamedNodeMap.cpp
DOM/Node.cpp
DOM/NodeIterator.cpp
DOM/NodeOperations.cpp
DOM/ParentNode.cpp
DOM/Position.cpp
@ -436,6 +437,7 @@ libweb_js_wrapper(DOM/EventTarget)
libweb_js_wrapper(DOM/HTMLCollection)
libweb_js_wrapper(DOM/NamedNodeMap)
libweb_js_wrapper(DOM/Node)
libweb_js_wrapper(DOM/NodeIterator)
libweb_js_wrapper(DOM/NodeList)
libweb_js_wrapper(DOM/ProcessingInstruction)
libweb_js_wrapper(DOM/Range)

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
@ -31,6 +31,7 @@
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/ExceptionOr.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/NodeIterator.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/Text.h>
@ -1460,4 +1461,10 @@ ExceptionOr<Document::PrefixAndTagName> Document::validate_qualified_name(String
};
}
// https://dom.spec.whatwg.org/#dom-document-createnodeiterator
NonnullRefPtr<NodeIterator> Document::create_node_iterator(Node& root, unsigned what_to_show, RefPtr<NodeFilter> filter)
{
return NodeIterator::create(root, what_to_show, move(filter));
}
}

View file

@ -324,6 +324,8 @@ public:
};
static ExceptionOr<PrefixAndTagName> validate_qualified_name(String const& qualified_name);
NonnullRefPtr<NodeIterator> create_node_iterator(Node& root, unsigned what_to_show, RefPtr<NodeFilter>);
private:
explicit Document(const AK::URL&);

View file

@ -7,6 +7,8 @@
#import <DOM/Event.idl>
#import <DOM/HTMLCollection.idl>
#import <DOM/Node.idl>
#import <DOM/NodeFilter.idl>
#import <DOM/NodeIterator.idl>
#import <DOM/NodeList.idl>
#import <DOM/Range.idl>
#import <DOM/Text.idl>
@ -173,4 +175,6 @@ interface Document : Node {
readonly boolean hidden;
readonly DOMString visibilityState;
[NewObject] NodeIterator createNodeIterator(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null);
};

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/RefCounted.h>
#include <LibJS/Heap/Handle.h>
#include <LibWeb/Bindings/CallbackType.h>
#include <LibWeb/Bindings/Wrappable.h>
namespace Web::DOM {
class NodeFilter
: public RefCounted<NodeFilter>
, public Bindings::Wrappable {
public:
using WrapperType = Bindings::NodeFilterWrapper;
explicit NodeFilter(Bindings::CallbackType callback)
: m_callback(move(callback))
{
}
virtual ~NodeFilter() = default;
Bindings::CallbackType& callback() { return m_callback; }
enum Result {
FILTER_ACCEPT = 1,
FILTER_REJECT = 2,
FILTER_SKIP = 3,
};
private:
Bindings::CallbackType m_callback;
};
inline JS::Object* wrap(JS::GlobalObject&, Web::DOM::NodeFilter& filter)
{
return filter.callback().callback.cell();
}
}

View file

@ -0,0 +1,30 @@
#import <DOM/Node.idl>
[Exposed=Window]
interface NodeFilter {
// FIXME: This should be a callback interface.
// Constants for acceptNode()
const unsigned short FILTER_ACCEPT = 1;
const unsigned short FILTER_REJECT = 2;
const unsigned short FILTER_SKIP = 3;
// Constants for whatToShow
const unsigned long SHOW_ALL = 0xFFFFFFFF;
const unsigned long SHOW_ELEMENT = 0x1;
const unsigned long SHOW_ATTRIBUTE = 0x2;
const unsigned long SHOW_TEXT = 0x4;
const unsigned long SHOW_CDATA_SECTION = 0x8;
const unsigned long SHOW_ENTITY_REFERENCE = 0x10; // legacy
const unsigned long SHOW_ENTITY = 0x20; // legacy
const unsigned long SHOW_PROCESSING_INSTRUCTION = 0x40;
const unsigned long SHOW_COMMENT = 0x80;
const unsigned long SHOW_DOCUMENT = 0x100;
const unsigned long SHOW_DOCUMENT_TYPE = 0x200;
const unsigned long SHOW_DOCUMENT_FRAGMENT = 0x400;
const unsigned long SHOW_NOTATION = 0x800; // legacy
unsigned short acceptNode(Node node);
};

View file

@ -0,0 +1,163 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/DOMExceptionWrapper.h>
#include <LibWeb/Bindings/IDLAbstractOperations.h>
#include <LibWeb/Bindings/NodeWrapper.h>
#include <LibWeb/Bindings/NodeWrapperFactory.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/DOM/NodeIterator.h>
namespace Web::DOM {
NodeIterator::NodeIterator(Node& root)
: m_root(root)
, m_reference(root)
{
}
// https://dom.spec.whatwg.org/#dom-document-createnodeiterator
NonnullRefPtr<NodeIterator> NodeIterator::create(Node& root, unsigned what_to_show, RefPtr<NodeFilter> filter)
{
// 1. Let iterator be a new NodeIterator object.
// 2. Set iterators root and iterators reference to root.
auto iterator = adopt_ref(*new NodeIterator(root));
// 3. Set iterators pointer before reference to true.
iterator->m_pointer_before_reference = true;
// 4. Set iterators whatToShow to whatToShow.
iterator->m_what_to_show = what_to_show;
// 5. Set iterators filter to filter.
iterator->m_filter = move(filter);
// 6. Return iterator.
return iterator;
}
// https://dom.spec.whatwg.org/#dom-nodeiterator-detach
void NodeIterator::detach()
{
// The detach() method steps are to do nothing.
// Its functionality (disabling a NodeIterator object) was removed, but the method itself is preserved for compatibility.
}
// https://dom.spec.whatwg.org/#concept-nodeiterator-traverse
JS::ThrowCompletionOr<RefPtr<Node>> NodeIterator::traverse(Direction direction)
{
// 1. Let node be iterators reference.
auto node = m_reference;
// 2. Let beforeNode be iterators pointer before reference.
auto before_node = m_pointer_before_reference;
// 3. While true:
while (true) {
// 4. Branch on direction:
if (direction == Direction::Next) {
// next
// If beforeNode is false, then set node to the first node following node in iterators iterator collection.
// If there is no such node, then return null.
if (!before_node) {
auto* next_node = node->next_in_pre_order(m_root.ptr());
if (!next_node)
return RefPtr<Node> {};
node = *next_node;
} else {
// If beforeNode is true, then set it to false.
before_node = false;
}
} else {
// previous
// If beforeNode is true, then set node to the first node preceding node in iterators iterator collection.
// If there is no such node, then return null.
if (before_node) {
if (node == m_root.ptr())
return nullptr;
auto* previous_node = node->previous_in_pre_order();
if (!previous_node)
return RefPtr<Node> {};
node = *previous_node;
} else {
// If beforeNode is false, then set it to true.
before_node = true;
}
}
// 2. Let result be the result of filtering node within iterator.
auto result = filter(*node);
if (result.is_throw_completion())
return result.release_error();
// 3. If result is FILTER_ACCEPT, then break.
if (result.value() == NodeFilter::FILTER_ACCEPT)
break;
}
// 4. Set iterators reference to node.
m_reference = node;
// 5. Set iterators pointer before reference to beforeNode.
m_pointer_before_reference = before_node;
// 6. Return node.
return RefPtr<Node> { node };
}
// https://dom.spec.whatwg.org/#concept-node-filter
JS::ThrowCompletionOr<NodeFilter::Result> NodeIterator::filter(Node& node)
{
VERIFY(wrapper());
auto& global_object = wrapper()->global_object();
// 1. If traversers active flag is set, then throw an "InvalidStateError" DOMException.
if (m_active)
return JS::throw_completion(wrap(global_object, InvalidStateError::create("NodeIterator is already active")));
// 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::FILTER_SKIP;
// 4. If traversers filter is null, then return FILTER_ACCEPT.
if (!m_filter)
return NodeFilter::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 = Bindings::IDL::call_user_object_operation(m_filter->callback(), "acceptNode", {}, wrap(global_object, 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(global_object));
return static_cast<NodeFilter::Result>(result_value);
}
// https://dom.spec.whatwg.org/#dom-nodeiterator-nextnode
JS::ThrowCompletionOr<RefPtr<Node>> NodeIterator::next_node()
{
return traverse(Direction::Next);
}
// https://dom.spec.whatwg.org/#dom-nodeiterator-previousnode
JS::ThrowCompletionOr<RefPtr<Node>> NodeIterator::previous_node()
{
return traverse(Direction::Previous);
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2022, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/RefCounted.h>
#include <LibWeb/Bindings/Wrappable.h>
#include <LibWeb/DOM/NodeFilter.h>
namespace Web::DOM {
// https://dom.spec.whatwg.org/#nodeiterator
class NodeIterator
: public RefCounted<NodeIterator>
, public Bindings::Wrappable {
public:
using WrapperType = Bindings::NodeIteratorWrapper;
static NonnullRefPtr<NodeIterator> create(Node& root, unsigned what_to_show, RefPtr<NodeFilter>);
NonnullRefPtr<Node> root() { return m_root; }
NonnullRefPtr<Node> reference_node() { return m_reference; }
bool pointer_before_reference_node() const { return m_pointer_before_reference; }
unsigned what_to_show() const { return m_what_to_show; }
NodeFilter* filter() { return m_filter; }
JS::ThrowCompletionOr<RefPtr<Node>> next_node();
JS::ThrowCompletionOr<RefPtr<Node>> previous_node();
void detach();
private:
NodeIterator(Node& root);
enum class Direction {
Next,
Previous,
};
JS::ThrowCompletionOr<RefPtr<Node>> traverse(Direction);
JS::ThrowCompletionOr<NodeFilter::Result> filter(Node&);
// https://dom.spec.whatwg.org/#concept-traversal-root
NonnullRefPtr<DOM::Node> m_root;
// https://dom.spec.whatwg.org/#nodeiterator-reference
NonnullRefPtr<DOM::Node> m_reference;
// https://dom.spec.whatwg.org/#nodeiterator-pointer-before-reference
bool m_pointer_before_reference { true };
// https://dom.spec.whatwg.org/#concept-traversal-whattoshow
unsigned m_what_to_show { 0 };
// https://dom.spec.whatwg.org/#concept-traversal-filter
RefPtr<DOM::NodeFilter> m_filter;
// https://dom.spec.whatwg.org/#concept-traversal-active
bool m_active { false };
};
}

View file

@ -0,0 +1,18 @@
#import <DOM/Node.idl>
#import <DOM/NodeFilter.idl>
[Exposed=Window]
interface NodeIterator {
[SameObject] readonly attribute Node root;
readonly attribute Node referenceNode;
readonly attribute boolean pointerBeforeReferenceNode;
readonly attribute unsigned long whatToShow;
readonly attribute NodeFilter? filter;
Node? nextNode();
Node? previousNode();
undefined detach();
};

View file

@ -110,6 +110,8 @@ class IDLEventListener;
class LiveNodeList;
class NamedNodeMap;
class Node;
class NodeFilter;
class NodeIterator;
class NodeList;
class ParentNode;
class Position;
@ -469,6 +471,8 @@ class MessageEventWrapper;
class MessagePortWrapper;
class MouseEventWrapper;
class NamedNodeMapWrapper;
class NodeFilterWrapper;
class NodeIteratorWrapper;
class NodeListWrapper;
class NodeWrapper;
class PageTransitionEventWrapper;