LibWeb: Implement Document named properties with light caching

We now cache potentially named elements on the Document when elements
are inserted and removed. This allows us to do lookup of what names are
supported much faster than if we had to iterate the tree every time.

This first cut doesn't implement the rules for 'exposed' object and
embed elements.
This commit is contained in:
Andrew Kaster 2024-02-15 14:43:44 -07:00 committed by Tim Flynn
parent faaf5b9652
commit 94149db073
Notes: sideshowbarker 2024-07-17 02:39:10 +09:00
6 changed files with 285 additions and 8 deletions

View file

@ -0,0 +1,16 @@
Submit <FORM >
document.bob === document.forms[0]: true
<BUTTON id="fred" >
img element with name 'foo' and id 'bar':
<IMG id="bar" >
img element with id 'baz', but no name:
baz === undefined: true
Multiple elements with name 'foo':
foos.length = 2
<IMG id="bar" >
<FORM >
obj element with name 'greg' and id 'banana':
<OBJECT id="banana" >
<OBJECT id="banana" >
goodbye greg/banana
no more greg or banana: true, true

View file

@ -0,0 +1,45 @@
<form name="bob">
<button type="submit" id="fred" name="george" value="submit">Submit</button>
</form>
<img name="foo" id="bar" src="http://www.example.com" alt="Example" />
<img id="baz" src="http://www.example.com" alt="Example" />
<form name="foo">
</form>
<script src="../include.js"></script>
<script>
test(() => {
printElement(document.bob);
println(`document.bob === document.forms[0]: ${document.bob === document.forms[0]}`);
printElement(document.bob.fred);
println("img element with name 'foo' and id 'bar':");
printElement(document.bar);
println("img element with id 'baz', but no name:");
let baz = document.baz;
println(`baz === undefined: ${baz === undefined}`);
println("Multiple elements with name 'foo':");
let foos = document.foo;
println(`foos.length = ${foos.length}`);
for (let i = 0; i < foos.length; i++) {
printElement(foos[i]);
}
let obj = document.createElement("object");
obj.name = "greg";
obj.id = "banana";
document.body.insertBefore(obj, document.foo[0]);
println("obj element with name 'greg' and id 'banana':");
printElement(document.greg);
printElement(document.banana);
println("goodbye greg/banana");
document.body.removeChild(document.greg);
println(`no more greg or banana: ${document.greg === undefined}, ${document.banana === undefined}`);
});
</script>

View file

@ -65,6 +65,7 @@
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLImageElement.h>
#include <LibWeb/HTML/HTMLLinkElement.h>
#include <LibWeb/HTML/HTMLObjectElement.h>
#include <LibWeb/HTML/HTMLScriptElement.h>
#include <LibWeb/HTML/HTMLTitleElement.h>
#include <LibWeb/HTML/ListOfAvailableImages.h>
@ -337,6 +338,11 @@ Document::Document(JS::Realm& realm, const AK::URL& url)
, m_style_computer(make<CSS::StyleComputer>(*this))
, m_url(url)
{
m_legacy_platform_object_flags = PlatformObject::LegacyPlatformObjectFlags {
.supports_named_properties = true,
.has_legacy_override_built_ins_interface_extended_attribute = true,
};
HTML::main_thread_event_loop().register_document({}, *this);
m_style_update_timer = Core::Timer::create_single_shot(0, [this] {
@ -456,6 +462,9 @@ void Document::visit_edges(Cell::Visitor& visitor)
for (auto* form_associated_element : m_form_associated_elements_with_form_attribute)
visitor.visit(form_associated_element->form_associated_element_to_html_element());
for (auto& element : m_potentially_named_elements)
visitor.visit(element);
}
// https://w3c.github.io/selection-api/#dom-document-getselection
@ -3714,16 +3723,87 @@ void Document::append_pending_animation_event(Web::DOM::Document::PendingAnimati
m_pending_animation_event_queue.append(event);
}
void Document::element_id_changed(Badge<DOM::Element>)
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter
static bool is_potentially_named_element(DOM::Element const& element)
{
return is<HTML::HTMLEmbedElement>(element) || is<HTML::HTMLFormElement>(element) || is<HTML::HTMLIFrameElement>(element) || is<HTML::HTMLImageElement>(element) || is<HTML::HTMLObjectElement>(element);
}
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter
static bool is_potentially_named_element_by_id(DOM::Element const& element)
{
return is<HTML::HTMLObjectElement>(element) || is<HTML::HTMLImageElement>(element);
}
static void insert_in_tree_order(Vector<JS::NonnullGCPtr<DOM::Element>>& elements, DOM::Element& element)
{
for (auto& el : elements) {
if (el == &element)
return;
}
auto index = elements.find_first_index_if([&](auto& existing_element) {
return existing_element->compare_document_position(element) & Node::DOCUMENT_POSITION_FOLLOWING;
});
if (index.has_value())
elements.insert(index.value(), element);
else
elements.append(element);
}
void Document::element_id_changed(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element)
{
for (auto* form_associated_element : m_form_associated_elements_with_form_attribute)
form_associated_element->element_id_changed({});
if (element->id().has_value())
insert_in_tree_order(m_potentially_named_elements, element);
else
(void)m_potentially_named_elements.remove_first_matching([element](auto& e) { return e == element; });
}
void Document::element_with_id_was_added_or_removed(Badge<DOM::Element>)
void Document::element_with_id_was_added(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element)
{
for (auto* form_associated_element : m_form_associated_elements_with_form_attribute)
form_associated_element->element_with_id_was_added_or_removed({});
if (is_potentially_named_element_by_id(*element))
insert_in_tree_order(m_potentially_named_elements, element);
}
void Document::element_with_id_was_removed(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element)
{
for (auto* form_associated_element : m_form_associated_elements_with_form_attribute)
form_associated_element->element_with_id_was_added_or_removed({});
if (is_potentially_named_element_by_id(*element))
(void)m_potentially_named_elements.remove_first_matching([element](auto& e) { return e == element; });
}
void Document::element_name_changed(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element)
{
if (element->name().has_value()) {
insert_in_tree_order(m_potentially_named_elements, element);
} else {
if (is_potentially_named_element_by_id(element) && element->id().has_value())
return;
(void)m_potentially_named_elements.remove_first_matching([element](auto& e) { return e == element; });
}
}
void Document::element_with_name_was_added(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element)
{
if (is_potentially_named_element(element))
insert_in_tree_order(m_potentially_named_elements, element);
}
void Document::element_with_name_was_removed(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element)
{
if (is_potentially_named_element(element)) {
if (is_potentially_named_element_by_id(element) && element->id().has_value())
return;
(void)m_potentially_named_elements.remove_first_matching([element](auto& e) { return e == element; });
}
}
void Document::add_form_associated_element_with_form_attribute(HTML::FormAssociatedElement& form_associated_element)
@ -3879,4 +3959,121 @@ JS::GCPtr<Element const> Document::scrolling_element() const
return nullptr;
}
// https://html.spec.whatwg.org/multipage/dom.html#exposed
static bool is_exposed(Element const& element)
{
if (is<HTML::HTMLObjectElement>(element) || is<HTML::HTMLEmbedElement>(element)) {
// FIXME: An embed or object element is said to be exposed if it has no exposed object ancestor, and,
// for object elements, is additionally either not showing its fallback content or has no object or embed descendants.
return true;
}
return true;
}
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-which
Vector<FlyString> Document::supported_property_names() const
{
// The supported property names of a Document object document at any moment consist of the following,
// in tree order according to the element that contributed them, ignoring later duplicates,
// and with values from id attributes coming before values from name attributes when the same element contributes both:
OrderedHashTable<FlyString> names;
// the value of the name content attribute for all exposed embed, form, iframe, img, and exposed object elements
// that have a non-empty name content attribute and are in a document tree with document as their root;
// the value of the id content attribute for all exposed object elements that have a non-empty id content attribute
// and are in a document tree with document as their root; and
// the value of the id content attribute for all img elements that have both a non-empty id content attribute
// and a non-empty name content attribute, and are in a document tree with document as their root.
for (auto const& element : m_potentially_named_elements) {
if (!is_exposed(element))
continue;
if (is<HTML::HTMLObjectElement>(*element)) {
if (auto id = element->id(); id.has_value())
names.set(id.value());
}
if (is<HTML::HTMLImageElement>(*element)) {
auto maybe_name = element->name();
// Only set id if both name and id have value, for img elements. clear as mud
if (auto maybe_id = element->id(); maybe_name.has_value() && maybe_id.has_value())
names.set(maybe_id.value());
}
if (auto name = element->name(); name.has_value())
names.set(name.value());
}
return names.values();
}
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem-filter
static Vector<JS::NonnullGCPtr<DOM::Element>> named_elements_with_name(Document const& document, FlyString const& name)
{
// Named elements with the name name, for the purposes of the above algorithm, are those that are either:
// - Exposed embed, form, iframe, img, or exposed object elements that have a name content attribute whose value is name, or
// - Exposed object elements that have an id content attribute whose value is name, or
// - img elements that have an id content attribute whose value is name, and that have a non-empty name content attribute present also.
Vector<JS::NonnullGCPtr<DOM::Element>> named_elements;
for (auto const& element : document.potentially_named_elements()) {
if (!is_exposed(*element))
continue;
if (is<HTML::HTMLObjectElement>(*element)) {
if (element->id() == name)
named_elements.append(element);
} else if (is<HTML::HTMLImageElement>(*element)) {
if (element->id() == name && element->name().has_value())
named_elements.append(element);
}
if (element->name() == name) {
named_elements.append(*element);
}
}
return named_elements;
}
// https://html.spec.whatwg.org/multipage/dom.html#dom-document-nameditem
WebIDL::ExceptionOr<JS::Value> Document::named_item_value(FlyString const& name) const
{
// 1. Let elements be the list of named elements with the name name that are in a document tree with the Document as their root.
// NOTE: There will be at least one such element, since the algorithm would otherwise not have been invoked by Web IDL.
auto elements = named_elements_with_name(*this, name);
// 2. If elements has only one element, and that element is an iframe element, and that iframe element's content navigable is not null,
// then return the active WindowProxy of the element's content navigable.
if (elements.size() == 1 && is<HTML::HTMLIFrameElement>(*elements.first())) {
auto& iframe_element = static_cast<HTML::HTMLIFrameElement&>(*elements.first());
if (iframe_element.content_navigable() != nullptr)
return iframe_element.content_navigable()->active_window_proxy();
}
// 3. Otherwise, if elements has only one element, return that element.
if (elements.size() == 1)
return elements.first();
// 4. Otherwise return an HTMLCollection rooted at the Document node, whose filter matches only named elements with the name name.
auto collection = HTMLCollection::create(*const_cast<Document*>(this), HTMLCollection::Scope::Descendants, [name](auto& element) {
if (!is_potentially_named_element(element) || !is_exposed(element))
return false;
if (is<HTML::HTMLObjectElement>(element)) {
if (element.id() == name)
return true;
} else if (is<HTML::HTMLImageElement>(element)) {
if (element.id() == name && element.name().has_value())
return true;
}
return (element.name() == name);
});
return collection;
}
}

View file

@ -555,8 +555,12 @@ public:
JS::GCPtr<HTML::SessionHistoryEntry> latest_entry() const { return m_latest_entry; }
void set_latest_entry(JS::GCPtr<HTML::SessionHistoryEntry> e) { m_latest_entry = e; }
void element_id_changed(Badge<DOM::Element>);
void element_with_id_was_added_or_removed(Badge<DOM::Element>);
void element_id_changed(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element);
void element_with_id_was_added(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element);
void element_with_id_was_removed(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element);
void element_name_changed(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element);
void element_with_name_was_added(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element);
void element_with_name_was_removed(Badge<DOM::Element>, JS::NonnullGCPtr<DOM::Element> element);
void add_form_associated_element_with_form_attribute(HTML::FormAssociatedElement&);
void remove_form_associated_element_with_form_attribute(HTML::FormAssociatedElement&);
@ -572,6 +576,10 @@ public:
void set_needs_to_resolve_paint_only_properties() { m_needs_to_resolve_paint_only_properties = true; }
virtual WebIDL::ExceptionOr<JS::Value> named_item_value(FlyString const& name) const override;
virtual Vector<FlyString> supported_property_names() const override;
Vector<JS::NonnullGCPtr<DOM::Element>> const& potentially_named_elements() const { return m_potentially_named_elements; }
protected:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
@ -796,6 +804,8 @@ private:
Vector<HTML::FormAssociatedElement*> m_form_associated_elements_with_form_attribute;
Vector<JS::NonnullGCPtr<DOM::Element>> m_potentially_named_elements;
bool m_design_mode_enabled { false };
bool m_needs_to_resolve_paint_only_properties { true };

View file

@ -24,7 +24,8 @@
#import <Selection/Selection.idl>
// https://dom.spec.whatwg.org/#document
[Exposed=Window]
// https://html.spec.whatwg.org/multipage/dom.html#the-document-object
[Exposed=Window, LegacyOverrideBuiltins]
interface Document : Node {
constructor();

View file

@ -466,12 +466,14 @@ void Element::attribute_changed(FlyString const& name, Optional<String> const& v
else
m_id = value_or_empty;
document().element_id_changed({});
document().element_id_changed({}, *this);
} else if (name == HTML::AttributeNames::name) {
if (!value.has_value())
m_name = {};
else
m_name = value_or_empty;
document().element_name_changed({}, *this);
} else if (name == HTML::AttributeNames::class_) {
auto new_classes = value_or_empty.bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace);
m_classes.clear();
@ -1033,7 +1035,10 @@ void Element::inserted()
Base::inserted();
if (m_id.has_value())
document().element_with_id_was_added_or_removed({});
document().element_with_id_was_added({}, *this);
if (m_name.has_value())
document().element_with_name_was_added({}, *this);
}
void Element::removed_from(Node* node)
@ -1041,7 +1046,10 @@ void Element::removed_from(Node* node)
Base::removed_from(node);
if (m_id.has_value())
document().element_with_id_was_added_or_removed({});
document().element_with_id_was_removed({}, *this);
if (m_name.has_value())
document().element_with_name_was_removed({}, *this);
}
void Element::children_changed()