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:
parent
faaf5b9652
commit
94149db073
Notes:
sideshowbarker
2024-07-17 02:39:10 +09:00
Author: https://github.com/ADKaster Commit: https://github.com/SerenityOS/serenity/commit/94149db073 Pull-request: https://github.com/SerenityOS/serenity/pull/23206 Issue: https://github.com/SerenityOS/serenity/issues/22232 Reviewed-by: https://github.com/trflynn89 ✅
6 changed files with 285 additions and 8 deletions
16
Tests/LibWeb/Text/expected/DOM/Document-named-properties.txt
Normal file
16
Tests/LibWeb/Text/expected/DOM/Document-named-properties.txt
Normal 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
|
45
Tests/LibWeb/Text/input/DOM/Document-named-properties.html
Normal file
45
Tests/LibWeb/Text/input/DOM/Document-named-properties.html
Normal 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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue