ladybird/Libraries/LibWeb/DOM/HTMLCollection.cpp
Aliaksandr Kalenik 73de6d4387 LibWeb: Use weak pointer for cache in HTMLCollection
This avoids keeping elements cached in an HTMLCollection alive longer
than necessary in the following scenario:
1. The HTMLCollection cache is populated by querying it.
2. Elements that were included in the cache are removed from the DOM.
3. The cached elements are kept alive by strong references in the cache
   until it is updated, which might never happen.
2025-12-17 11:41:21 +01:00

186 lines
6.2 KiB
C++
Raw Permalink 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) 2021-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/InsertionSort.h>
#include <LibWeb/Bindings/HTMLCollectionPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/ParentNode.h>
#include <LibWeb/Namespace.h>
namespace Web::DOM {
GC_DEFINE_ALLOCATOR(HTMLCollection);
GC::Ref<HTMLCollection> HTMLCollection::create(ParentNode& root, Scope scope, Function<bool(Element const&)> filter, Function<bool(Element const&, Element const&)> sort)
{
return root.realm().create<HTMLCollection>(root, scope, move(filter), move(sort));
}
HTMLCollection::HTMLCollection(ParentNode& root, Scope scope, Function<bool(Element const&)> filter, Function<bool(Element const&, Element const&)> sort)
: PlatformObject(root.realm())
, m_root(root)
, m_filter(move(filter))
, m_sort(move(sort))
, 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,
};
}
HTMLCollection::~HTMLCollection() = default;
void HTMLCollection::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLCollection);
Base::initialize(realm);
}
void HTMLCollection::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_root);
}
void HTMLCollection::update_name_to_element_mappings_if_needed() const
{
update_cache_if_needed();
if (m_cached_name_to_element_mappings)
return;
m_cached_name_to_element_mappings = make<OrderedHashMap<FlyString, GC::Weak<Element>>>();
for (auto const& element : m_cached_elements) {
// 1. If element has an ID which is not in result, append elements ID to result.
if (auto const& id = element->id(); id.has_value()) {
if (!id.value().is_empty() && !m_cached_name_to_element_mappings->contains(id.value()))
m_cached_name_to_element_mappings->set(id.value(), element);
}
// 2. If element is in the HTML namespace and has a name attribute whose value is neither the empty string nor is in result, append elements name attribute value to result.
if (element->namespace_uri() == Namespace::HTML && element->name().has_value()) {
auto element_name = element->name().value();
if (!element_name.is_empty() && !m_cached_name_to_element_mappings->contains(element_name))
m_cached_name_to_element_mappings->set(move(element_name), element);
}
}
}
void HTMLCollection::update_cache_if_needed() const
{
// Nothing to do, the DOM hasn't updated since we last built the cache.
if (m_cached_dom_tree_version == root()->document().dom_tree_version())
return;
m_cached_elements.clear();
m_cached_name_to_element_mappings = nullptr;
if (m_scope == Scope::Descendants) {
m_root->for_each_in_subtree_of_type<Element>([&](auto& element) {
if (m_filter(element))
m_cached_elements.append(element);
return TraversalDecision::Continue;
});
} else {
m_root->for_each_child_of_type<Element>([&](auto& element) {
if (m_filter(element))
m_cached_elements.append(element);
return IterationDecision::Continue;
});
}
if (m_sort) {
insertion_sort(m_cached_elements, [this](auto const& a, auto const& b) {
return this->m_sort(*a, *b);
});
}
m_cached_dom_tree_version = root()->document().dom_tree_version();
}
GC::RootVector<GC::Ref<Element>> HTMLCollection::collect_matching_elements() const
{
update_cache_if_needed();
GC::RootVector<GC::Ref<Element>> elements(heap());
for (auto& element : m_cached_elements)
elements.append(*element);
return elements;
}
// https://dom.spec.whatwg.org/#dom-htmlcollection-length
size_t HTMLCollection::length() const
{
// The length getter steps are to return the number of nodes represented by the collection.
update_cache_if_needed();
return m_cached_elements.size();
}
// https://dom.spec.whatwg.org/#dom-htmlcollection-item
Element* HTMLCollection::item(size_t index) const
{
// The item(index) method steps are to return the indexth element in the collection. If there is no indexth element in the collection, then the method must return null.
update_cache_if_needed();
if (index >= m_cached_elements.size())
return nullptr;
return m_cached_elements[index];
}
// https://dom.spec.whatwg.org/#dom-htmlcollection-nameditem-key
Element* HTMLCollection::named_item(FlyString const& key) const
{
// 1. If key is the empty string, return null.
if (key.is_empty())
return nullptr;
update_name_to_element_mappings_if_needed();
if (auto it = m_cached_name_to_element_mappings->get(key); it.has_value())
return it.value();
return nullptr;
}
// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names
bool HTMLCollection::is_supported_property_name(FlyString const& name) const
{
update_name_to_element_mappings_if_needed();
return m_cached_name_to_element_mappings->contains(name);
}
// https://dom.spec.whatwg.org/#ref-for-dfn-supported-property-names
Vector<FlyString> HTMLCollection::supported_property_names() const
{
// 1. Let result be an empty list.
Vector<FlyString> result;
// 2. For each element represented by the collection, in tree order:
update_name_to_element_mappings_if_needed();
for (auto const& it : *m_cached_name_to_element_mappings) {
result.append(it.key);
}
// 3. Return result.
return result;
}
Optional<JS::Value> HTMLCollection::item_value(size_t index) const
{
auto* element = item(index);
if (!element)
return {};
return element;
}
JS::Value HTMLCollection::named_item_value(FlyString const& name) const
{
auto* element = named_item(name);
if (!element)
return JS::js_undefined();
return element;
}
}