From fabcee016f42f7d56d76496b2a86d7e28ba5b385 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 9 Mar 2022 14:34:32 +0100 Subject: [PATCH] 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. --- .../LibWeb/WrapperGenerator/IDLGenerators.cpp | 14 +- .../LibWeb/Bindings/WindowObjectHelper.h | 3 + Userland/Libraries/LibWeb/CMakeLists.txt | 2 + Userland/Libraries/LibWeb/DOM/Document.cpp | 9 +- Userland/Libraries/LibWeb/DOM/Document.h | 2 + Userland/Libraries/LibWeb/DOM/Document.idl | 4 + Userland/Libraries/LibWeb/DOM/NodeFilter.h | 46 +++++ Userland/Libraries/LibWeb/DOM/NodeFilter.idl | 30 ++++ .../Libraries/LibWeb/DOM/NodeIterator.cpp | 163 ++++++++++++++++++ Userland/Libraries/LibWeb/DOM/NodeIterator.h | 67 +++++++ .../Libraries/LibWeb/DOM/NodeIterator.idl | 18 ++ Userland/Libraries/LibWeb/Forward.h | 4 + 12 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 Userland/Libraries/LibWeb/DOM/NodeFilter.h create mode 100644 Userland/Libraries/LibWeb/DOM/NodeFilter.idl create mode 100644 Userland/Libraries/LibWeb/DOM/NodeIterator.cpp create mode 100644 Userland/Libraries/LibWeb/DOM/NodeIterator.h create mode 100644 Userland/Libraries/LibWeb/DOM/NodeIterator.idl diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator/IDLGenerators.cpp index 2d979e28c6..f5372124bb 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/WrapperGenerator/IDLGenerators.cpp @@ -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 @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(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(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 #include #include +#include #include #include #include diff --git a/Userland/Libraries/LibWeb/Bindings/WindowObjectHelper.h b/Userland/Libraries/LibWeb/Bindings/WindowObjectHelper.h index f30f345391..1c0d12339d 100644 --- a/Userland/Libraries/LibWeb/Bindings/WindowObjectHelper.h +++ b/Userland/Libraries/LibWeb/Bindings/WindowObjectHelper.h @@ -233,6 +233,8 @@ #include #include #include +#include +#include #include #include #include @@ -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) \ diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 1b16cac009..eef6d1fbc7 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -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) diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index af4be273d3..a1d8df1fad 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021, Andreas Kling + * Copyright (c) 2018-2022, Andreas Kling * Copyright (c) 2021, Linus Groh * Copyright (c) 2021, Luke Wilde * Copyright (c) 2021, Sam Atkins @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -1460,4 +1461,10 @@ ExceptionOr Document::validate_qualified_name(String }; } +// https://dom.spec.whatwg.org/#dom-document-createnodeiterator +NonnullRefPtr Document::create_node_iterator(Node& root, unsigned what_to_show, RefPtr filter) +{ + return NodeIterator::create(root, what_to_show, move(filter)); +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index 6da28d8a62..299ce9d4b1 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -324,6 +324,8 @@ public: }; static ExceptionOr validate_qualified_name(String const& qualified_name); + NonnullRefPtr create_node_iterator(Node& root, unsigned what_to_show, RefPtr); + private: explicit Document(const AK::URL&); diff --git a/Userland/Libraries/LibWeb/DOM/Document.idl b/Userland/Libraries/LibWeb/DOM/Document.idl index 50b1baceef..e7b9401e85 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.idl +++ b/Userland/Libraries/LibWeb/DOM/Document.idl @@ -7,6 +7,8 @@ #import #import #import +#import +#import #import #import #import @@ -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); + }; diff --git a/Userland/Libraries/LibWeb/DOM/NodeFilter.h b/Userland/Libraries/LibWeb/DOM/NodeFilter.h new file mode 100644 index 0000000000..47f530e99e --- /dev/null +++ b/Userland/Libraries/LibWeb/DOM/NodeFilter.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Web::DOM { + +class NodeFilter + : public RefCounted + , 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(); +} + +} diff --git a/Userland/Libraries/LibWeb/DOM/NodeFilter.idl b/Userland/Libraries/LibWeb/DOM/NodeFilter.idl new file mode 100644 index 0000000000..94215112f9 --- /dev/null +++ b/Userland/Libraries/LibWeb/DOM/NodeFilter.idl @@ -0,0 +1,30 @@ +#import + +[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); + +}; diff --git a/Userland/Libraries/LibWeb/DOM/NodeIterator.cpp b/Userland/Libraries/LibWeb/DOM/NodeIterator.cpp new file mode 100644 index 0000000000..0abba71695 --- /dev/null +++ b/Userland/Libraries/LibWeb/DOM/NodeIterator.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2022, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Web::DOM { + +NodeIterator::NodeIterator(Node& root) + : m_root(root) + , m_reference(root) +{ +} + +// https://dom.spec.whatwg.org/#dom-document-createnodeiterator +NonnullRefPtr NodeIterator::create(Node& root, unsigned what_to_show, RefPtr filter) +{ + // 1. Let iterator be a new NodeIterator object. + // 2. Set iterator’s root and iterator’s reference to root. + auto iterator = adopt_ref(*new NodeIterator(root)); + + // 3. Set iterator’s pointer before reference to true. + iterator->m_pointer_before_reference = true; + + // 4. Set iterator’s whatToShow to whatToShow. + iterator->m_what_to_show = what_to_show; + + // 5. Set iterator’s 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> NodeIterator::traverse(Direction direction) +{ + // 1. Let node be iterator’s reference. + auto node = m_reference; + + // 2. Let beforeNode be iterator’s 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 iterator’s 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 = *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 iterator’s 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 = *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 iterator’s reference to node. + m_reference = node; + + // 5. Set iterator’s pointer before reference to beforeNode. + m_pointer_before_reference = before_node; + + // 6. Return node. + return RefPtr { node }; +} + +// https://dom.spec.whatwg.org/#concept-node-filter +JS::ThrowCompletionOr NodeIterator::filter(Node& node) +{ + VERIFY(wrapper()); + auto& global_object = wrapper()->global_object(); + + // 1. If traverser’s 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 node’s nodeType attribute value − 1. + auto n = node.node_type() - 1; + + // 3. If the nth bit (where 0 is the least significant bit) of traverser’s whatToShow is not set, then return FILTER_SKIP. + if (!(m_what_to_show & (1u << n))) + return NodeFilter::FILTER_SKIP; + + // 4. If traverser’s filter is null, then return FILTER_ACCEPT. + if (!m_filter) + return NodeFilter::FILTER_ACCEPT; + + // 5. Set traverser’s active flag. + m_active = true; + + // 6. Let result be the return value of call a user object’s operation with traverser’s filter, "acceptNode", and « node ». + // If this throws an exception, then unset traverser’s 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 traverser’s active flag. + m_active = false; + + // 8. Return result. + auto result_value = TRY(result.value()->to_i32(global_object)); + return static_cast(result_value); +} + +// https://dom.spec.whatwg.org/#dom-nodeiterator-nextnode +JS::ThrowCompletionOr> NodeIterator::next_node() +{ + return traverse(Direction::Next); +} + +// https://dom.spec.whatwg.org/#dom-nodeiterator-previousnode +JS::ThrowCompletionOr> NodeIterator::previous_node() +{ + return traverse(Direction::Previous); +} + +} diff --git a/Userland/Libraries/LibWeb/DOM/NodeIterator.h b/Userland/Libraries/LibWeb/DOM/NodeIterator.h new file mode 100644 index 0000000000..4df146617c --- /dev/null +++ b/Userland/Libraries/LibWeb/DOM/NodeIterator.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::DOM { + +// https://dom.spec.whatwg.org/#nodeiterator +class NodeIterator + : public RefCounted + , public Bindings::Wrappable { +public: + using WrapperType = Bindings::NodeIteratorWrapper; + + static NonnullRefPtr create(Node& root, unsigned what_to_show, RefPtr); + + NonnullRefPtr root() { return m_root; } + NonnullRefPtr 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> next_node(); + JS::ThrowCompletionOr> previous_node(); + + void detach(); + +private: + NodeIterator(Node& root); + + enum class Direction { + Next, + Previous, + }; + + JS::ThrowCompletionOr> traverse(Direction); + + JS::ThrowCompletionOr filter(Node&); + + // https://dom.spec.whatwg.org/#concept-traversal-root + NonnullRefPtr m_root; + + // https://dom.spec.whatwg.org/#nodeiterator-reference + NonnullRefPtr 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 m_filter; + + // https://dom.spec.whatwg.org/#concept-traversal-active + bool m_active { false }; +}; + +} diff --git a/Userland/Libraries/LibWeb/DOM/NodeIterator.idl b/Userland/Libraries/LibWeb/DOM/NodeIterator.idl new file mode 100644 index 0000000000..b46a41ff45 --- /dev/null +++ b/Userland/Libraries/LibWeb/DOM/NodeIterator.idl @@ -0,0 +1,18 @@ +#import +#import + +[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(); + +}; diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 5ce0491446..f64a3ad00a 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -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;