From 1f59e21829ee44af2ed5a934643b75d9b206720b Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Mon, 1 Apr 2024 08:44:24 +0200 Subject: [PATCH] LibWeb: Implement HTMLAllCollection This collection has some pretty strange behaviour, particularly with the IsHTMLDDA slot which is defined in the javascript spec specifically for this object. This commit implements pretty much all of this interface, besides from the custom [[Call]]. There is also no caching over this collection. Since it is a live collection over the entire document, the performance is never going to be great, and I am not convinced any speedup for this legacy interface is worth a massive cache. --- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + Userland/Libraries/LibWeb/Forward.h | 1 + .../LibWeb/HTML/HTMLAllCollection.cpp | 233 ++++++++++++++++++ .../Libraries/LibWeb/HTML/HTMLAllCollection.h | 63 +++++ .../LibWeb/HTML/HTMLAllCollection.idl | 13 + Userland/Libraries/LibWeb/idl_files.cmake | 1 + 6 files changed, 312 insertions(+) create mode 100644 Userland/Libraries/LibWeb/HTML/HTMLAllCollection.cpp create mode 100644 Userland/Libraries/LibWeb/HTML/HTMLAllCollection.h create mode 100644 Userland/Libraries/LibWeb/HTML/HTMLAllCollection.idl diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 589f28cdfd3..8e4080a6cd6 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -277,6 +277,7 @@ set(SOURCES HTML/FormDataEvent.cpp HTML/GlobalEventHandlers.cpp HTML/History.cpp + HTML/HTMLAllCollection.cpp HTML/HTMLAnchorElement.cpp HTML/HTMLAreaElement.cpp HTML/HTMLAudioElement.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 7500d06640f..810f26ec1b7 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -347,6 +347,7 @@ class EventLoop; class FormAssociatedElement; class FormDataEvent; class History; +class HTMLAllCollection; class HTMLAnchorElement; class HTMLAreaElement; class HTMLAudioElement; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.cpp b/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.cpp new file mode 100644 index 00000000000..2fa30d7c264 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2024, Shannon Booth + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +JS_DEFINE_ALLOCATOR(HTMLAllCollection); + +JS::NonnullGCPtr HTMLAllCollection::create(DOM::ParentNode& root, Scope scope, Function filter) +{ + return root.heap().allocate(root.realm(), root, scope, move(filter)); +} + +HTMLAllCollection::HTMLAllCollection(DOM::ParentNode& root, Scope scope, Function filter) + : PlatformObject(root.realm()) + , m_root(root) + , m_filter(move(filter)) + , m_scope(scope) +{ + m_legacy_platform_object_flags = LegacyPlatformObjectFlags { + .supports_indexed_properties = true, + .supports_named_properties = true, + .has_legacy_unenumerable_named_properties_interface_extended_attribute = true, + }; +} + +HTMLAllCollection::~HTMLAllCollection() = default; + +void HTMLAllCollection::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLAllCollection); +} + +void HTMLAllCollection::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_root); +} + +// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#all-named-elements +static bool is_all_named_element(DOM::Element const& element) +{ + // The following elements are "all"-named elements: a, button, embed, form, frame, frameset, iframe, img, input, map, meta, object, select, and textarea + return is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element) + || is(element); +} + +JS::MarkedVector> HTMLAllCollection::collect_matching_elements() const +{ + JS::MarkedVector> elements(m_root->heap()); + if (m_scope == Scope::Descendants) { + m_root->for_each_in_subtree_of_type([&](auto& element) { + if (m_filter(element)) + elements.append(element); + return IterationDecision::Continue; + }); + } else { + m_root->for_each_child_of_type([&](auto& element) { + if (m_filter(element)) + elements.append(element); + return IterationDecision::Continue; + }); + } + return elements; +} + +// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#dom-htmlallcollection-length +size_t HTMLAllCollection::length() const +{ + // The length getter steps are to return the number of nodes represented by the collection. + return collect_matching_elements().size(); +} + +// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#dom-htmlallcollection-item +Variant, JS::NonnullGCPtr, Empty> HTMLAllCollection::item(Optional const& name_or_index) const +{ + // 1. If nameOrIndex was not provided, return null. + if (!name_or_index.has_value()) + return Empty {}; + + // 2. Return the result of getting the "all"-indexed or named element(s) from this, given nameOrIndex. + return get_the_all_indexed_or_named_elements(name_or_index.value()); +} + +// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#dom-htmlallcollection-nameditem +Variant, JS::NonnullGCPtr, Empty> HTMLAllCollection::named_item(FlyString const& name) const +{ + // The namedItem(name) method steps are to return the result of getting the "all"-named element(s) from this given name. + return get_the_all_named_elements(name); +} + +// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names +Vector HTMLAllCollection::supported_property_names() const +{ + // The supported property names consist of the non-empty values of all the id attributes of all the + // elements represented by the collection, and the non-empty values of all the name attributes of + // all the "all"-named elements represented by the collection, in tree order, ignoring later duplicates, + // with the id of an element preceding its name if it contributes both, they differ from each other, and + // neither is the duplicate of an earlier entry. + + Vector result; + auto elements = collect_matching_elements(); + + for (auto const& element : elements) { + if (auto const& id = element->id(); id.has_value() && !id->is_empty()) { + if (!result.contains_slow(id.value())) + result.append(id.value()); + } + + if (is_all_named_element(*element) && element->name().has_value() && !element->name()->is_empty()) { + auto name = element->name().value(); + if (!name.is_empty() && !result.contains_slow(name)) + result.append(move(name)); + } + } + + // 3. Return result. + return result; +} + +// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#concept-get-all-named +Variant, JS::NonnullGCPtr, Empty> HTMLAllCollection::get_the_all_named_elements(FlyString const& name) const +{ + // 1. If name is the empty string, return null. + if (name.is_empty()) + return Empty {}; + + // 2. Let subCollection be an HTMLCollection object rooted at the same Document as collection, whose filter matches only elements that are either: + auto sub_collection = DOM::HTMLCollection::create(m_root, DOM::HTMLCollection::Scope::Descendants, [name](DOM::Element const& element) { + // * "all"-named elements with a name attribute equal to name, or, + if (is_all_named_element(element) && element.name() == name) + return true; + + // * elements with an ID equal to name. + return element.id() == name; + }); + + // 3. If there is exactly one element in subCollection, then return that element. + auto matching_elements = sub_collection->collect_matching_elements(); + if (matching_elements.size() == 1) + return matching_elements.first(); + + // 4. Otherwise, if subCollection is empty, return null. + if (matching_elements.is_empty()) + return Empty {}; + + // 5. Otherwise, return subCollection. + return sub_collection; +} + +// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#concept-get-all-indexed +JS::GCPtr HTMLAllCollection::get_the_all_indexed_element(u32 index) const +{ + // To get the "all"-indexed element from an HTMLAllCollection collection given an index index, return the indexth + // element in collection, or null if there is no such indexth element. + auto elements = collect_matching_elements(); + if (index >= elements.size()) + return nullptr; + return elements[index]; +} + +// https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#concept-get-all-indexed-or-named +Variant, JS::NonnullGCPtr, Empty> HTMLAllCollection::get_the_all_indexed_or_named_elements(JS::PropertyKey const& name_or_index) const +{ + // 1. If nameOrIndex, converted to a JavaScript String value, is an array index property name, return the result of getting the "all"-indexed element from + // collection given the number represented by nameOrIndex. + if (name_or_index.is_number()) { + auto maybe_element = get_the_all_indexed_element(name_or_index.as_number()); + if (!maybe_element) + return Empty {}; + return JS::NonnullGCPtr { *maybe_element }; + } + + // 2. Return the result of getting the "all"-named element(s) from collection given nameOrIndex. + return get_the_all_named_elements(MUST(FlyString::from_deprecated_fly_string(name_or_index.as_string()))); +} + +bool HTMLAllCollection::is_supported_property_index(u32 index) const +{ + return index < collect_matching_elements().size(); +} + +WebIDL::ExceptionOr HTMLAllCollection::item_value(size_t index) const +{ + return get_the_all_indexed_element(index); +} + +WebIDL::ExceptionOr HTMLAllCollection::named_item_value(FlyString const& name) const +{ + return named_item(name).visit( + [](Empty) -> JS::Value { return JS::js_undefined(); }, + [](auto const& value) -> JS::Value { return value; }); +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.h b/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.h new file mode 100644 index 00000000000..a904aec5636 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024, Shannon Booth + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +// FIXME: Should be part of HTML namespace! + +class HTMLAllCollection : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(HTMLAllCollection, Bindings::PlatformObject); + JS_DECLARE_ALLOCATOR(HTMLAllCollection); + +public: + enum class Scope { + Children, + Descendants, + }; + [[nodiscard]] static JS::NonnullGCPtr create(DOM::ParentNode& root, Scope, Function filter); + + virtual ~HTMLAllCollection() override; + + size_t length() const; + Variant, JS::NonnullGCPtr, Empty> item(Optional const& name_or_index) const; + Variant, JS::NonnullGCPtr, Empty> named_item(FlyString const& name) const; + + JS::MarkedVector> collect_matching_elements() const; + + virtual WebIDL::ExceptionOr item_value(size_t index) const override; + virtual WebIDL::ExceptionOr named_item_value(FlyString const& name) const override; + virtual Vector supported_property_names() const override; + virtual bool is_supported_property_index(u32) const override; + +protected: + HTMLAllCollection(DOM::ParentNode& root, Scope, Function filter); + + virtual void initialize(JS::Realm&) override; + + virtual bool is_htmldda() const override { return true; } + +private: + Variant, JS::NonnullGCPtr, Empty> get_the_all_named_elements(FlyString const& name) const; + JS::GCPtr get_the_all_indexed_element(u32 index) const; + Variant, JS::NonnullGCPtr, Empty> get_the_all_indexed_or_named_elements(JS::PropertyKey const& name_or_index) const; + + virtual void visit_edges(Cell::Visitor&) override; + + JS::NonnullGCPtr m_root; + Function m_filter; + Scope m_scope { Scope::Descendants }; +}; + +} diff --git a/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.idl b/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.idl new file mode 100644 index 00000000000..5a125ac3f37 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/HTMLAllCollection.idl @@ -0,0 +1,13 @@ +#import +#import + +[Exposed=Window, + LegacyUnenumerableNamedProperties] +interface HTMLAllCollection { + readonly attribute unsigned long length; + getter Element (unsigned long index); + getter (HTMLCollection or Element)? namedItem([FlyString] DOMString name); + (HTMLCollection or Element)? item(optional [FlyString] DOMString nameOrIndex); + + // Note: HTMLAllCollection objects have a custom [[Call]] internal method and an [[IsHTMLDDA]] internal slot. +}; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 69998765287..ee7cab7f602 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -102,6 +102,7 @@ libweb_js_bindings(HTML/DataTransfer) libweb_js_bindings(HTML/ErrorEvent) libweb_js_bindings(HTML/FormDataEvent) libweb_js_bindings(HTML/History) +libweb_js_bindings(HTML/HTMLAllCollection) libweb_js_bindings(HTML/HTMLAnchorElement) libweb_js_bindings(HTML/HTMLAreaElement) libweb_js_bindings(HTML/HTMLAudioElement)